使用Java從零開(kāi)始創(chuàng)建區(qū)塊鏈

使用Java從零開(kāi)始創(chuàng)建區(qū)塊鏈

image.png

目前網(wǎng)絡(luò)上關(guān)于區(qū)塊鏈入門(mén)、科普的文章不少,本文就不再贅述區(qū)塊鏈的基本概念了,如果對(duì)區(qū)塊鏈不是很了解的話,可以看一下我之前收集的一些入門(mén)學(xué)習(xí)資源:

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

對(duì)區(qū)塊鏈技術(shù)感到新奇的我們,都想知道區(qū)塊鏈在代碼上是怎么實(shí)現(xiàn)的,所以本文是實(shí)戰(zhàn)向的,畢竟理論我們都看了不少,但是對(duì)于區(qū)塊鏈具體的實(shí)現(xiàn)還不是很清楚,本文就使用Java語(yǔ)言來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的區(qū)塊鏈。

但是要完全搞懂區(qū)塊鏈并非易事,對(duì)于一門(mén)較為陌生的技術(shù),我們需要在理論+實(shí)踐中學(xué)習(xí),通過(guò)寫(xiě)代碼來(lái)學(xué)習(xí)技術(shù)會(huì)掌握得更牢固,構(gòu)建一個(gè)區(qū)塊鏈可以加深對(duì)區(qū)塊鏈的理解。

準(zhǔn)備工作

掌握基本的JavaSE以及JavaWeb開(kāi)發(fā),能夠使用Java開(kāi)發(fā)簡(jiǎn)單的項(xiàng)目,并且需要了解HTTP協(xié)議。

我們知道區(qū)塊鏈?zhǔn)怯蓞^(qū)塊的記錄構(gòu)成的不可變、有序的鏈結(jié)構(gòu),記錄可以是交易、文件或任何你想要的數(shù)據(jù),重要的是它們是通過(guò)哈希值(hashes)鏈接起來(lái)的。

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

環(huán)境描述

  • JDK1.8
  • Tomcat 9.0
  • Maven 3.5
  • JSON 20160810
  • javaee-api 7.0

pom.xml文件配置內(nèi)容:

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

然后還需要一個(gè)HTTP客戶端,比如Postman,Linux命令行下的curl或其它客戶端,我這里使用的是Postman。

Blockchain類(lèi)
首先創(chuàng)建一個(gè)Blockchain類(lèi),在構(gòu)造器中創(chuàng)建了兩個(gè)主要的集合,一個(gè)用于儲(chǔ)存區(qū)塊鏈,一個(gè)用于儲(chǔ)存交易列表,本文中所有核心的主要代碼都寫(xiě)在這個(gè)類(lèi)里,方便隨時(shí)查看,在實(shí)際開(kāi)發(fā)則不宜這么做,應(yīng)該把代碼拆分仔細(xì)降低耦合度。

以下是Blockchain類(lèi)的框架代碼:

package org.zero01.dao;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class BlockChain {

    // 存儲(chǔ)區(qū)塊鏈
    private List<Object> chain;
    // 該實(shí)例變量用于當(dāng)前的交易信息列表
    private List<Object> currentTransactions;

    public BlockChain() {
        // 初始化區(qū)塊鏈以及當(dāng)前的交易信息列表
        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類(lèi)用來(lái)管理區(qū)塊鏈,它能存儲(chǔ)交易,加入新塊等,下面我們來(lái)進(jìn)一步完善這些方法。

區(qū)塊的結(jié)構(gòu)

首先需要說(shuō)明一下區(qū)塊的結(jié)構(gòu),每個(gè)區(qū)塊包含屬性:索引(index),時(shí)間戳(timestamp),交易列表(transactions),工作量證明(稍后解釋?zhuān)┮约扒耙粋€(gè)區(qū)塊的Hash值。

以下是一個(gè)區(qū)塊的結(jié)構(gòu):

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

到這里,區(qū)塊鏈的概念就清楚了,每個(gè)新的區(qū)塊都包含上一個(gè)區(qū)塊的Hash,這是關(guān)鍵的一點(diǎn),它保障了區(qū)塊鏈不可變性。如果攻擊者破壞了前面的某個(gè)區(qū)塊,那么后面所有區(qū)塊的Hash都會(huì)變得不正確。不理解的話,慢慢消化,可以參考區(qū)塊鏈記賬原理。

由于需要計(jì)算區(qū)塊的hash,所以我們得先編寫(xiě)一個(gè)用于計(jì)算hash值的工具類(lèi):

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 加密開(kāi)始
                // 創(chuàng)建加密對(duì)象,傳入加密類(lèi)型
                MessageDigest messageDigest = MessageDigest.getInstance(strType);
                // 傳入要加密的字符串
                messageDigest.update(strText.getBytes());
                // 得到 byte 數(shù)組
                byte byteBuffer[] = messageDigest.digest();

                // 將 byte 數(shù)組轉(zhuǎn)換 string 類(lèi)型
                StringBuffer strHexString = new StringBuffer();
                // 遍歷 byte 數(shù)組
                for (int i = 0; i < byteBuffer.length; i++) {
                    // 轉(zhuǎn)換成16進(jìn)制并存儲(chǔ)在字符串中
                    String hex = Integer.toHexString(0xff & byteBuffer[i]);
                    if (hex.length() == 1) {
                        strHexString.append('0');
                    }
                    strHexString.append(hex);
                }
                // 得到返回結(jié)果
                strResult = strHexString.toString();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        }

        return strResult;
    }
}

加入交易功能

接下來(lái)我們需要實(shí)現(xiàn)一個(gè)交易\記賬功能,所以來(lái)完善newTransactions以及l(fā)astBlock方法:

    /**
     * @return 得到區(qū)塊鏈中的最后一個(gè)區(qū)塊
     */
    public HashMap<String, Object> lastBlock() {
        return getChain().get(getChain().size() - 1);
    }
    
    /**
     * 生成新交易信息,信息將加入到下一個(gè)待挖的區(qū)塊中
     * 
     * @param sender
     *            發(fā)送方的地址
     * @param recipient
     *            接收方的地址
     * @param amount
     *            交易數(shù)量
     * @return 返回存儲(chǔ)該交易事務(wù)的塊的索引
     */
    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方法向列表中添加一個(gè)交易記錄,并返回該記錄將被添加到的區(qū)塊 (下一個(gè)待挖掘的區(qū)塊)的索引,等下在用戶提交交易時(shí)會(huì)有用。

創(chuàng)建新塊

當(dāng)Blockchain實(shí)例化后,我們需要構(gòu)造一個(gè)創(chuàng)世區(qū)塊(沒(méi)有前區(qū)塊的第一個(gè)區(qū)塊),并且給它加上一個(gè)工作量證明。
每個(gè)區(qū)塊都需要經(jīng)過(guò)工作量證明,俗稱(chēng)挖礦,稍后會(huì)繼續(xù)講解。

為了構(gòu)造創(chuàng)世塊,我們還需要完善剩下的幾個(gè)方法,并且把該類(lèi)設(shè)計(jì)為單例:

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 {

    // 存儲(chǔ)區(qū)塊鏈
    private List<Map<String, Object>> chain;
    // 該實(shí)例變量用于當(dāng)前的交易信息列表
    private List<Map<String, Object>> currentTransactions;
    private static BlockChain blockChain = null;

    private BlockChain() {
        // 初始化區(qū)塊鏈以及當(dāng)前的交易信息列表
        chain = new ArrayList<Map<String, Object>>();
        currentTransactions = new ArrayList<Map<String, Object>>();

        // 創(chuàng)建創(chuàng)世區(qū)塊
        newBlock(100, "0");
    }
    
    // 創(chuàng)建單例對(duì)象
    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ū)塊鏈中的最后一個(gè)區(qū)塊
     */
    public Map<String, Object> lastBlock() {
        return getChain().get(getChain().size() - 1);
    }

    /**
     * 在區(qū)塊鏈上新建一個(gè)區(qū)塊
     * 
     * @param proof
     *            新區(qū)塊的工作量證明
     * @param previous_hash
     *            上一個(gè)區(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);
        // 如果沒(méi)有傳遞上一個(gè)區(qū)塊的hash就計(jì)算出區(qū)塊鏈中最后一個(gè)區(qū)塊的hash
        block.put("previous_hash", previous_hash != null ? previous_hash : hash(getChain().get(getChain().size() - 1)));

        // 重置當(dāng)前的交易信息列表
        setCurrentTransactions(new ArrayList<Map<String, Object>>());

        getChain().add(block);

        return block;
    }

    /**
     * 生成新交易信息,信息將加入到下一個(gè)待挖的區(qū)塊中
     * 
     * @param sender
     *            發(fā)送方的地址
     * @param recipient
     *            接收方的地址
     * @param amount
     *            交易數(shù)量
     * @return 返回該交易事務(wù)的塊的索引
     */
    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());
    }
}

通過(guò)上面的代碼和注釋可以對(duì)區(qū)塊鏈有直觀的了解,接下來(lái)我們來(lái)編寫(xiě)一些簡(jiǎn)單的測(cè)試代碼來(lái)測(cè)試一下這些代碼能否正常工作:

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();

        // 一個(gè)區(qū)塊中可以不包含任何交易記錄
        Map<String, Object> block = blockChain.newBlock(300, null);
        System.out.println(new JSONObject(block));

        // 一個(gè)區(qū)塊中可以包含一筆交易記錄
        blockChain.newTransactions("123", "222", 33);
        Map<String, Object> block1 = blockChain.newBlock(500, null);
        System.out.println(new JSONObject(block1));

        // 一個(gè)區(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));

        // 查看整個(gè)區(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));
    }
}

運(yùn)行結(jié)果:

// 挖出來(lái)的新區(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"
}

// 整個(gè)區(qū)塊鏈,第一個(gè)是創(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
}

通過(guò)以上的測(cè)試,可以很直觀的看到區(qū)塊鏈的數(shù)據(jù),但是現(xiàn)在只是完成了初步的代碼編寫(xiě),還有幾件事情還沒(méi)做,接下來(lái)我們看看區(qū)塊是怎么挖出來(lái)的。

理解工作量證明
新的區(qū)塊依賴工作量證明算法(PoW)來(lái)構(gòu)造。PoW的目標(biāo)是找出一個(gè)符合特定條件的數(shù)字,這個(gè)數(shù)字很難計(jì)算出來(lái),但容易驗(yàn)證。這就是工作量證明的核心思想。

為了方便理解,舉個(gè)例子:

假設(shè)一個(gè)整數(shù) x 乘以另一個(gè)整數(shù) y 的積的 Hash 值必須以 0 結(jié)尾,即 hash(x * y) = ac23dc…0。設(shè)變量 x = 5,求 y 的值?

用Java實(shí)現(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);
    }
}

結(jié)果是 y=21 ,因?yàn)椋?/p>

hash(5 * 21) = 1253e9373e...5e3600155e860

在比特幣中,使用稱(chēng)為Hashcash的工作量證明算法,它和上面的問(wèn)題很類(lèi)似。礦工們?yōu)榱藸?zhēng)奪創(chuàng)建區(qū)塊的權(quán)利而爭(zhēng)相計(jì)算結(jié)果。通常,計(jì)算難度與目標(biāo)字符串需要滿足的特定字符的數(shù)量成正比,礦工算出結(jié)果后,會(huì)獲得比特幣獎(jiǎng)勵(lì)。
當(dāng)然,在網(wǎng)絡(luò)上非常容易驗(yàn)證這個(gè)結(jié)果。

實(shí)現(xiàn)工作量證明
讓我們來(lái)實(shí)現(xiàn)一個(gè)相似PoW算法,規(guī)則是:尋找一個(gè)數(shù) p,使得它與前一個(gè)區(qū)塊的 proof 拼接成的字符串的 Hash 值以 4 個(gè)零開(kāi)頭:

    ...
    /**
     * 簡(jiǎn)單的工作量證明: 
     *   - 查找一個(gè) p' 使得 hash(pp') 以4個(gè)0開(kāi)頭 
     *   - p 是上一個(gè)塊的證明, p' 是當(dāng)前的證明
     *   
     * @param last_proof
     *               上一個(gè)塊的證明
     * @return
     */
    public long proofOfWork(long last_proof) {
        long proof = 0;
        while (!validProof(last_proof, proof)) {
            proof += 1;
        }
        return proof;
    }

    /**
     * 驗(yàn)證證明: 是否hash(last_proof, proof)以4個(gè)0開(kāi)頭?
     * 
     * @param last_proof
     *            上一個(gè)塊的證明
     * @param proof
     *            當(dāng)前的證明
     * @return 以4個(gè)0開(kāi)頭返回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");
    }

衡量算法復(fù)雜度的辦法是修改零開(kāi)頭的個(gè)數(shù)。使用4個(gè)來(lái)用于演示,你會(huì)發(fā)現(xiàn)多一個(gè)零都會(huì)大大增加計(jì)算出結(jié)果所需的時(shí)間。

現(xiàn)在Blockchain類(lèi)基本已經(jīng)完成了,接下來(lái)使用Servlet接收HTTP請(qǐng)求來(lái)進(jìn)行交互。

Blockchain作為API接口
我們將使用Java Web中的Servlet來(lái)接收用戶的HTTP請(qǐng)求,通過(guò)Servlet我們可以方便的將網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)映射到相應(yīng)的方法上進(jìn)行處理,現(xiàn)在我們來(lái)讓Blockchain運(yùn)行在基于Java Web上。

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

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

注冊(cè)節(jié)點(diǎn)ID
我們的“Tomcat服務(wù)器”將扮演區(qū)塊鏈網(wǎng)絡(luò)中的一個(gè)節(jié)點(diǎn),而每個(gè)節(jié)點(diǎn)都需要有一個(gè)唯一的標(biāo)識(shí)符,也就是id。在這里我們使用UUID來(lái)作為節(jié)點(diǎn)ID,我們需要在服務(wù)器啟動(dòng)時(shí),將UUID設(shè)置到ServletContext屬性中,這樣我們的服務(wù)器就擁有了唯一標(biāo)識(shí),這一步我們可以配置監(jiān)聽(tīng)類(lèi)來(lái)完成,首先配置web.xml文件內(nèi)容如下:

<?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ě)一個(gè)類(lèi)實(shí)現(xiàn)ServletContextListener接口,在初始化方法中把uuid設(shè)置到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類(lèi)
我們這里沒(méi)有使用任何框架,所以我們需要通過(guò)最基本的Servlet來(lái)接收并處理用戶的HTTP請(qǐng)求:

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用于運(yùn)行工作算法的證明來(lái)獲得下一個(gè)證明,也就是所謂的挖礦
@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用于輸出整個(gè)區(qū)塊鏈的數(shù)據(jù)
@WebServlet("/chain")
public class FullChain extends HttpServlet{

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

我們先來(lái)完成最簡(jiǎn)單的FullChain的代碼,這個(gè)Servlet用于向客戶端輸出整個(gè)區(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用于輸出整個(gè)區(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ù)的功能,每一個(gè)區(qū)塊都可以記錄交易數(shù)據(jù),發(fā)送到節(jié)點(diǎn)的交易數(shù)據(jù)結(jié)構(gòu)如下:

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

實(shí)現(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");
        // 讀取客戶端傳遞過(guò)來(lái)的數(shù)據(jù)并轉(zhuǎn)換成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)) {
                // 如果沒(méi)有需要的字段就返回錯(cuò)誤信息
                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();
    }
}

挖礦
挖礦正是神奇所在,它很簡(jiǎn)單,只做了以下三件事:

  • 計(jì)算工作量證明PoW
  • 通過(guò)新增一個(gè)交易授予礦工(自己)一個(gè)幣
  • 構(gòu)造新區(qū)塊并將其添加到鏈中

代碼實(shí)現(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用于運(yùn)行工作算法的證明來(lái)獲得下一個(gè)證明,也就是所謂的挖礦
@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é)點(diǎn)提供獎(jiǎng)勵(lì),發(fā)送者為 "0" 表明是新挖出的幣
        String uuid = (String) this.getServletContext().getAttribute("uuid");
        blockChain.newTransactions("0", uuid, 1);

        // 構(gòu)建新的區(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();
    }
}

注意交易的接收者是我們自己的服務(wù)器節(jié)點(diǎn),我們做的大部分工作都只是圍繞Blockchain類(lèi)的方法進(jìn)行交互。到此,我們的區(qū)塊鏈就算完成了,我們來(lái)實(shí)際運(yùn)行下。

運(yùn)行區(qū)塊鏈
由于我們這里也沒(méi)有寫(xiě)前端的web頁(yè)面,只寫(xiě)了后端的API,所以只能使用 Postman 之類(lèi)的軟件去和API進(jìn)行交互。首先啟動(dòng)Tomcat服務(wù)器,然后通過(guò)post請(qǐng)求 http://localhost:8089/BlockChain_Java/transactions/new 來(lái)添加新的交易信息(注意我這里沒(méi)有使用默認(rèn)的8080端口,默認(rèn)的情況下是8080端口):

image.png

但是這時(shí)候還沒(méi)有新的區(qū)塊可以寫(xiě)入這個(gè)交易信息,所以我們還需要請(qǐng)求 http://localhost:8089/BlockChain_Java/mine 來(lái)進(jìn)行挖礦,挖出一個(gè)新的區(qū)塊來(lái)存儲(chǔ)這筆交易:

image.png

在挖了兩次礦之后,就有3個(gè)塊了,通過(guò)請(qǐng)求 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
}

一致性(共識(shí))
我們已經(jīng)有了一個(gè)基本的區(qū)塊鏈可以接受交易和挖礦。但是區(qū)塊鏈系統(tǒng)應(yīng)該是分布式的。既然是分布式的,那么我們究竟拿什么保證所有節(jié)點(diǎn)有同樣的鏈呢?這就是一致性問(wèn)題,我們要想在網(wǎng)絡(luò)上有多個(gè)節(jié)點(diǎn),就必須實(shí)現(xiàn)一個(gè)一致性的算法。

注冊(cè)節(jié)點(diǎn)
在實(shí)現(xiàn)一致性算法之前,我們需要找到一種方式讓一個(gè)節(jié)點(diǎn)知道它相鄰的節(jié)點(diǎn)。每個(gè)節(jié)點(diǎn)都需要保存一份包含網(wǎng)絡(luò)中其它節(jié)點(diǎn)的記錄。因此讓我們新增幾個(gè)接口:

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

我們需要修改下BlockChain的構(gòu)造函數(shù)并提供一個(gè)注冊(cè)節(jié)點(diǎn)方法:

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

    private Set<String> nodes;
    private BlockChain() {
        ...
        // 用于存儲(chǔ)網(wǎng)絡(luò)中其他節(jié)點(diǎn)的集合
        nodes = new HashSet<String>();
        ...
    }
    
    public Set<String> getNodes() {
        return nodes;
    }
    
    /**
     * 注冊(cè)節(jié)點(diǎn)
     * 
     * @param address
     *            節(jié)點(diǎn)地址
     * @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 集合來(lái)儲(chǔ)存節(jié)點(diǎn),這是一種避免出現(xiàn)重復(fù)添加節(jié)點(diǎn)的簡(jiǎn)單方法。

實(shí)現(xiàn)共識(shí)算法
前面提到,沖突是指不同的節(jié)點(diǎn)擁有不同的鏈,為了解決這個(gè)問(wèn)題,規(guī)定最長(zhǎng)的、有效的鏈才是最終的鏈,換句話說(shuō),網(wǎng)絡(luò)中有效最長(zhǎng)鏈才是實(shí)際的鏈。

我們使用以下算法,來(lái)達(dá)到網(wǎng)絡(luò)中的共識(shí):

...
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
...

public class BlockChain {
    ...
    /**
     * 檢查是否是有效鏈,遍歷每個(gè)區(qū)塊驗(yàn)證hash和proof,來(lái)確定一個(gè)給定的區(qū)塊鏈?zhǔn)欠裼行?     * 
     * @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;
    }

    /**
     * 共識(shí)算法解決沖突,使用網(wǎng)絡(luò)中最長(zhǎng)的鏈. 遍歷所有的鄰居節(jié)點(diǎn),并用上一個(gè)方法檢查鏈的有效性, 如果發(fā)現(xiàn)有效更長(zhǎng)鏈,就替換掉自己的鏈
     * 
     * @return 如果鏈被取代返回true, 否則返回false
     * @throws IOException
     */
    public boolean resolveConflicts() throws IOException {
        Set<String> neighbours = this.nodes;
        List<Map<String, Object>> newChain = null;

        // 尋找最長(zhǎng)的區(qū)塊鏈
        long maxLength = this.chain.size();

        // 獲取并驗(yàn)證網(wǎng)絡(luò)中的所有節(jié)點(diǎn)的區(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();

                // 檢查長(zhǎng)度是否長(zhǎng),鏈?zhǔn)欠裼行?                if (length > maxLength && validChain(chain)) {
                    maxLength = length;
                    newChain = chain;
                }
            }

        }
        // 如果發(fā)現(xiàn)一個(gè)新的有效鏈比我們的長(zhǎng),就替換當(dāng)前的鏈
        if (newChain != null) {
            this.chain = newChain;
            return true;
        }
        return false;
    }
    ...

第一個(gè)方法 validChain() 用來(lái)檢查是否是有效鏈,遍歷每個(gè)塊驗(yàn)證hash和proof.

第2個(gè)方法 resolveConflicts() 用來(lái)解決沖突,遍歷所有的鄰居節(jié)點(diǎn),并用上一個(gè)方法檢查鏈的有效性, 如果發(fā)現(xiàn)有效更長(zhǎng)鏈,就替換掉自己的鏈

讓我們添加兩個(gè)Servlet,一個(gè)用來(lái)注冊(cè)節(jié)點(diǎn),一個(gè)用來(lái)解決沖突:

注冊(cè)節(jié)點(diǎn):

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;

// 用于注冊(cè)節(jié)點(diǎn)的Servlet
@WebServlet("/nodes/register")
public class NodesRegister extends HttpServlet {

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

        req.setCharacterEncoding("utf-8");
        // 讀取客戶端傳遞過(guò)來(lái)的數(shù)據(jù)并轉(zhuǎn)換成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é)點(diǎn)集合數(shù)據(jù),并進(jìn)行判空
        List<String> nodes = (List) jsonValues.getJSONArray("nodes").toList();
        if (nodes == null) {
            resp.sendError(400, "Error: Please supply a valid list of nodes");
        }

        // 注冊(cè)節(jié)點(diǎn)
        BlockChain blockChain = BlockChain.getInstance();
        for (String address : nodes) {
            blockChain.registerNode(address);
        }

        // 向客戶端返回處理結(jié)果
        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();
    }
}

我們可以在不同的機(jī)器運(yùn)行節(jié)點(diǎn),或在一臺(tái)機(jī)機(jī)開(kāi)啟不同的網(wǎng)絡(luò)端口來(lái)模擬多節(jié)點(diǎn)的網(wǎng)絡(luò),這里在同一臺(tái)機(jī)器開(kāi)啟不同的端口演示,配置兩個(gè)不同端口的服務(wù)器即可,我這里啟動(dòng)了兩個(gè)節(jié)點(diǎn):http://localhost:8089http://localhost:8066

兩個(gè)節(jié)點(diǎn)互相進(jìn)行注冊(cè):


image.png

image.png

然后在8066節(jié)點(diǎn)上挖兩個(gè)塊,確保是更長(zhǎng)的鏈:


image.png

接著在8089節(jié)點(diǎn)上訪問(wèn)接口/nodes/resolve ,這時(shí)8089節(jié)點(diǎn)的鏈會(huì)通過(guò)共識(shí)算法被8066節(jié)點(diǎn)的鏈取代:


image.png

通過(guò)共識(shí)算法保持一致性后,兩個(gè)節(jié)點(diǎn)的區(qū)塊鏈數(shù)據(jù)就都是一致的了:


image.png

image.png

到此為止我們就完成了一個(gè)區(qū)塊鏈的開(kāi)發(fā),雖然這只是一個(gè)最基本的區(qū)塊鏈,而且在開(kāi)發(fā)的過(guò)程中也沒(méi)有考慮太多的程序設(shè)計(jì)方面的問(wèn)題,而是以最基本、原始的方式進(jìn)行開(kāi)發(fā)的。但是我們不妨以這個(gè)簡(jiǎn)單的區(qū)塊鏈為基礎(chǔ),發(fā)揮自己的能力動(dòng)手去重構(gòu)、擴(kuò)展、完善這個(gè)區(qū)塊鏈程序,直至成為自己的一個(gè)小項(xiàng)目。


本文項(xiàng)目代碼地址如下:

https://github.com/Binary-ZeroOne/blockchain-java-demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容