使用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ò)工作量證明哲虾,俗稱挖礦,稍后會(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

在比特幣中奢米,使用稱為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)系作者
  • 序言:七十年代末脆诉,一起剝皮案震驚了整個(gè)濱河市甚亭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌击胜,老刑警劉巖亏狰,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異偶摔,居然都是意外死亡暇唾,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)辰斋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)策州,“玉大人,你說(shuō)我怎么就攤上這事宫仗」还遥” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵藕夫,是天一觀的道長(zhǎng)孽糖。 經(jīng)常有香客問(wèn)我,道長(zhǎng)汁胆,這世上最難降的妖魔是什么梭姓? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮嫩码,結(jié)果婚禮上誉尖,老公的妹妹穿的比我還像新娘。我一直安慰自己铸题,他們只是感情好铡恕,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著丢间,像睡著了一般探熔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上烘挫,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天诀艰,我揣著相機(jī)與錄音柬甥,去河邊找鬼。 笑死其垄,一個(gè)胖子當(dāng)著我的面吹牛苛蒲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播绿满,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼臂外,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了喇颁?” 一聲冷哼從身側(cè)響起漏健,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎橘霎,沒(méi)想到半個(gè)月后蔫浆,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡茎毁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年克懊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片七蜘。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖墙懂,靈堂內(nèi)的尸體忽然破棺而出橡卤,到底是詐尸還是另有隱情,我是刑警寧澤损搬,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布碧库,位于F島的核電站,受9級(jí)特大地震影響巧勤,放射性物質(zhì)發(fā)生泄漏嵌灰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一颅悉、第九天 我趴在偏房一處隱蔽的房頂上張望沽瞭。 院中可真熱鬧,春花似錦剩瓶、人聲如沸驹溃。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)豌鹤。三九已至,卻和暖如春枝缔,著一層夾襖步出監(jiān)牢的瞬間布疙,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灵临,地道東北人截型。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像俱诸,于是被迫代替她去往敵國(guó)和親菠劝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容