用 Go 構(gòu)建一個區(qū)塊鏈 -- Part 3: 持久化和命令行接口

翻譯的系列文章我已經(jīng)放到了 GitHub 上:blockchain-tutorial涧偷,后續(xù)如有更新都會在 GitHub 上,可能就不在這里同步了毙死。如果想直接運行代碼燎潮,也可以 clone GitHub 上的教程倉庫,進(jìn)入 src 目錄執(zhí)行 make 即可扼倘。


引言

到目前為止,我們已經(jīng)構(gòu)建了一個有工作量證明機(jī)制的區(qū)塊鏈。有了工作量證明浑厚,挖礦也就有了著落宏邮。雖然目前的實現(xiàn)離一個有著完整功能的區(qū)塊鏈越來越近了,但是它仍然缺少了一些重要的特性纠拔。在今天的內(nèi)容中秉剑,我們會將區(qū)塊鏈持久化到一個數(shù)據(jù)庫中,然后會提供一個簡單的命令行接口稠诲,用來完成一些與區(qū)塊鏈的交互操作侦鹏。本質(zhì)上,區(qū)塊鏈?zhǔn)且粋€分布式數(shù)據(jù)庫臀叙,不過略水,我們暫時先忽略 “分布式” 這個部分,僅專注于 “存儲” 這一點劝萤。

選擇數(shù)據(jù)庫

目前渊涝,我們的區(qū)塊鏈實現(xiàn)里面并沒有用到數(shù)據(jù)庫,而是在每次運行程序時稳其,簡單地將區(qū)塊鏈存儲在內(nèi)存中驶赏。那么一旦程序退出,所有的內(nèi)容就都消失了既鞠。我們沒有辦法再次使用這條鏈煤傍,也沒有辦法與其他人共享,所以我們需要把它存儲到磁盤上嘱蛋。

那么蚯姆,我們要用哪個數(shù)據(jù)庫呢?實際上洒敏,任何一個數(shù)據(jù)庫都可以龄恋。在 比特幣原始論文 中,并沒有提到要使用哪一個具體的數(shù)據(jù)庫凶伙,它完全取決于開發(fā)者如何選擇郭毕。 Bitcoin Core ,最初由中本聰發(fā)布函荣,現(xiàn)在是比特幣的一個參考實現(xiàn)显押,它使用的是 LevelDB扳肛。而我們將要使用的是...

BoltDB

因為它:

  1. 非常簡單和簡約
  2. 用 Go 實現(xiàn)
  3. 不需要運行一個服務(wù)器
  4. 能夠允許我們構(gòu)造想要的數(shù)據(jù)結(jié)構(gòu)

BoltDB GitHub 上的 README 是這么說的:

Bolt 是一個純鍵值存儲的 Go 數(shù)據(jù)庫,啟發(fā)自 Howard Chu 的 LMDB. 它旨在為那些無須一個像 Postgres 和 MySQL 這樣有著完整數(shù)據(jù)庫服務(wù)器的項目乘碑,提供一個簡單挖息,快速和可靠的數(shù)據(jù)庫。

由于 Bolt 意在用于提供一些底層功能兽肤,簡潔便成為其關(guān)鍵所在套腹。它的
API 并不多,并且僅關(guān)注值的獲取和設(shè)置资铡。僅此而已电禀。

聽起來跟我們的需求完美契合!來快速過一下:

Bolt 使用鍵值存儲害驹,這意味著它沒有像 SQL RDBMS (MySQL鞭呕,PostgreSQL 等等)的表,沒有行和列宛官。相反葫松,數(shù)據(jù)被存儲為鍵值對(key-value pair,就像 Golang 的 map)底洗。鍵值對被存儲在 bucket 中腋么,這是為了將相似的鍵值對進(jìn)行分組(類似 RDBMS 中的表格)。因此亥揖,為了獲取一個值珊擂,你需要知道一個 bucket 和一個鍵(key)。

需要注意的一個事情是费变,Bolt 數(shù)據(jù)庫沒有數(shù)據(jù)類型:鍵和值都是字節(jié)數(shù)組(byte array)摧扇。鑒于需要在里面存儲 Go 的結(jié)構(gòu)(準(zhǔn)確來說,也就是存儲(塊)Block)挚歧,我們需要對它們進(jìn)行序列化扛稽,也就說,實現(xiàn)一個從 Go struct 轉(zhuǎn)換到一個 byte array 的機(jī)制滑负,同時還可以從一個 byte array 再轉(zhuǎn)換回 Go struct在张。雖然我們將會使用 encoding/gob 來完成這一目標(biāo),但實際上也可以選擇使用 JSON, XML, Protocol Buffers 等等矮慕。之所以選擇使用 encoding/gob, 是因為它很簡單帮匾,而且是 Go 標(biāo)準(zhǔn)庫的一部分。

數(shù)據(jù)庫結(jié)構(gòu)

在開始實現(xiàn)持久化的邏輯之前痴鳄,我們首先需要決定到底要如何在數(shù)據(jù)庫中進(jìn)行存儲瘟斜。為此,我們可以參考 Bitcoin Core 的做法:

簡單來說,Bitcoin Core 使用兩個 “bucket” 來存儲數(shù)據(jù):

  1. 其中一個 bucket 是 blocks哼转,它存儲了描述一條鏈中所有塊的元數(shù)據(jù)
  2. 另一個 bucket 是 chainstate明未,存儲了一條鏈的狀態(tài)槽华,也就是當(dāng)前所有的未花費的交易輸出壹蔓,和一些元數(shù)據(jù)

此外,出于性能的考慮猫态,Bitcoin Core 將每個區(qū)塊(block)存儲為磁盤上的不同文件佣蓉。如此一來,就不需要僅僅為了讀取一個單一的塊而將所有(或者部分)的塊都加載到內(nèi)存中亲雪。但是勇凭,為了簡單起見,我們并不會實現(xiàn)這一點义辕。

blocks 中虾标,key -> value 為:

key value
b + 32 字節(jié)的 block hash block index record
f + 4 字節(jié)的 file number file information record
l + 4 字節(jié)的 file number the last block file number used
R + 1 字節(jié)的 boolean 是否正在 reindex
F + 1 字節(jié)的 flag name length + flag name string 1 byte boolean: various flags that can be on or off
t + 32 字節(jié)的 transaction hash transaction index record

chainstatekey -> value 為:

key value
c + 32 字節(jié)的 transaction hash unspent transaction output record for that transaction
B 32 字節(jié)的 block hash: the block hash up to which the database represents the unspent transaction outputs

詳情可見 這里灌砖。

因為目前還沒有交易璧函,所以我們只需要 blocks bucket。另外基显,正如上面提到的蘸吓,我們會將整個數(shù)據(jù)庫存儲為單個文件,而不是將區(qū)塊存儲在不同的文件中撩幽。所以库继,我們也不會需要文件編號(file number)相關(guān)的東西。最終窜醉,我們會用到的鍵值對有:

  1. 32 字節(jié)的 block-hash -> block 結(jié)構(gòu)
  2. l -> 鏈中最后一個塊的 hash

這就是實現(xiàn)持久化機(jī)制所有需要了解的內(nèi)容了宪萄。

序列化

上面提到,在 BoltDB 中榨惰,值只能是 []byte 類型拜英,但是我們想要存儲 Block 結(jié)構(gòu)。所以读串,我們需要使用 encoding/gob 來對這些結(jié)構(gòu)進(jìn)行序列化聊记。

讓我們來實現(xiàn) BlockSerialize 方法(為了簡潔起見,此處略去了錯誤處理):

func (b *Block) Serialize() []byte {
    var result bytes.Buffer
    encoder := gob.NewEncoder(&result)

    err := encoder.Encode(b)

    return result.Bytes()
}

這個部分比較直觀:首先恢暖,我們定義一個 buffer 存儲序列化之后的數(shù)據(jù)排监。然后,我們初始化一個 gob encoder 并對 block 進(jìn)行編碼杰捂,結(jié)果作為一個字節(jié)數(shù)組返回舆床。

接下來,我們需要一個解序列化的函數(shù),它會接受一個字節(jié)數(shù)組作為輸入挨队,并返回一個 Block. 它不是一個方法(method)谷暮,而是一個單獨的函數(shù)(function):

func DeserializeBlock(d []byte) *Block {
    var block Block

    decoder := gob.NewDecoder(bytes.NewReader(d))
    err := decoder.Decode(&block)

    return &block
}

這就是序列化部分的內(nèi)容了。

持久化

讓我們從 NewBlockchain 函數(shù)開始盛垦。在之前的實現(xiàn)中湿弦,它會創(chuàng)建一個新的
Blockchain 實例,并向其中加入創(chuàng)世塊腾夯。而現(xiàn)在颊埃,我們希望它做的事情有:

  1. 打開一個數(shù)據(jù)庫文件
  2. 檢查文件里面是否已經(jīng)存儲了一個區(qū)塊鏈
  3. 如果已經(jīng)存儲了一個區(qū)塊鏈:
    1. 創(chuàng)建一個新的 Blockchain 實例
    2. 設(shè)置 Blockchain 實例的 tip 為數(shù)據(jù)庫中存儲的最后一個塊的哈希
  4. 如果沒有區(qū)塊鏈:
    1. 創(chuàng)建創(chuàng)世塊
    2. 存儲到數(shù)據(jù)庫
    3. 將創(chuàng)世塊哈希保存為最后一個塊的哈希
    4. 創(chuàng)建一個新的 Blockchain 實例,其 tip 指向創(chuàng)世塊(tip 有尾部蝶俱,尖端的意思班利,在這里 tip 存儲的是最后一個塊的哈希)

代碼大概是這樣:

func NewBlockchain() *Blockchain {
    var tip []byte
    db, err := bolt.Open(dbFile, 0600, nil)

    err = db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))

        if b == nil {
            genesis := NewGenesisBlock()
            b, err := tx.CreateBucket([]byte(blocksBucket))
            err = b.Put(genesis.Hash, genesis.Serialize())
            err = b.Put([]byte("l"), genesis.Hash)
            tip = genesis.Hash
        } else {
            tip = b.Get([]byte("l"))
        }

        return nil
    })

    bc := Blockchain{tip, db}

    return &bc
}

來一段一段地看下代碼:

db, err := bolt.Open(dbFile, 0600, nil)

這是打開一個 BoltDB 文件的標(biāo)準(zhǔn)做法。注意榨呆,即使不存在這樣的文件罗标,它也不會返回錯誤。

err = db.Update(func(tx *bolt.Tx) error {
...
})

在 BoltDB 中积蜻,數(shù)據(jù)庫操作通過一個事務(wù)(transaction)進(jìn)行操作闯割。有兩種類型的事務(wù):只讀(read-only)和讀寫(read-write)。這里浅侨,打開的是一個讀寫事務(wù)(db.Update(...))纽谒,因為我們可能會向數(shù)據(jù)庫中添加創(chuàng)世塊。

b := tx.Bucket([]byte(blocksBucket))

if b == nil {
    genesis := NewGenesisBlock()
    b, err := tx.CreateBucket([]byte(blocksBucket))
    err = b.Put(genesis.Hash, genesis.Serialize())
    err = b.Put([]byte("l"), genesis.Hash)
    tip = genesis.Hash
} else {
    tip = b.Get([]byte("l"))
}

這里是函數(shù)的核心如输。在這里鼓黔,我們先獲取了存儲區(qū)塊的 bucket:如果存在,就從中讀取 l 鍵不见;如果不存在澳化,就生成創(chuàng)世塊,創(chuàng)建 bucket稳吮,并將區(qū)塊保存到里面缎谷,然后更新 l 鍵以存儲鏈中最后一個塊的哈希。

另外灶似,注意創(chuàng)建 Blockchain 一個新的方式:

bc := Blockchain{tip, db}

這次列林,我們不在里面存儲所有的區(qū)塊了,而是僅存儲區(qū)塊鏈的 tip酪惭。另外希痴,我們存儲了一個數(shù)據(jù)庫連接。因為我們想要一旦打開它的話春感,就讓它一直運行砌创,直到程序運行結(jié)束虏缸。因此,Blockchain 的結(jié)構(gòu)現(xiàn)在看起來是這樣:

type Blockchain struct {
    tip []byte
    db  *bolt.DB
}

接下來我們想要更新的是 AddBlock 方法:現(xiàn)在向鏈中加入?yún)^(qū)塊嫩实,就不是像之前向一個數(shù)組中加入一個元素那么簡單了刽辙。從現(xiàn)在開始,我們會將區(qū)塊存儲在數(shù)據(jù)庫里面:

func (bc *Blockchain) AddBlock(data string) {
    var lastHash []byte

    err := bc.db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        lastHash = b.Get([]byte("l"))

        return nil
    })

    newBlock := NewBlock(data, lastHash)

    err = bc.db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        err := b.Put(newBlock.Hash, newBlock.Serialize())
        err = b.Put([]byte("l"), newBlock.Hash)
        bc.tip = newBlock.Hash

        return nil
    })
}

繼續(xù)來一段一段分解開來:

err := bc.db.View(func(tx *bolt.Tx) error {
    b := tx.Bucket([]byte(blocksBucket))
    lastHash = b.Get([]byte("l"))

    return nil
})

這是 BoltDB 事務(wù)的另一個類型(只讀)甲献。在這里宰缤,我們會從數(shù)據(jù)庫中獲取最后一個塊的哈希,然后用它來挖出一個新的塊的哈希:

newBlock := NewBlock(data, lastHash)
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash

檢查區(qū)塊鏈

現(xiàn)在竟纳,產(chǎn)生的所有塊都會被保存到一個數(shù)據(jù)庫里面撵溃,所以我們可以重新打開一個鏈,然后向里面加入新塊锥累。但是在實現(xiàn)這一點后,我們失去了之前一個非常好的特性:我們再也無法打印區(qū)塊鏈的區(qū)塊了集歇,因為現(xiàn)在不是將區(qū)塊存儲在一個數(shù)組桶略,而是放到了數(shù)據(jù)庫里面。讓我們來解決這個問題诲宇!

BoltDB 允許對一個 bucket 里面的所有 key 進(jìn)行迭代际歼,但是所有的 key 都以字節(jié)序進(jìn)行存儲,而且我們想要以區(qū)塊能夠進(jìn)入?yún)^(qū)塊鏈中的順序進(jìn)行打印姑蓝。此外鹅心,因為我們不想將所有的塊都加載到內(nèi)存中(因為我們的區(qū)塊鏈數(shù)據(jù)庫可能很大!或者現(xiàn)在可以假裝它可能很大)纺荧,我們將會一個一個地讀取它們旭愧。故而,我們需要一個區(qū)塊鏈迭代器(BlockchainIterator):

type BlockchainIterator struct {
    currentHash []byte
    db          *bolt.DB
}

每當(dāng)要對鏈中的塊進(jìn)行迭代時宙暇,我們就會創(chuàng)建一個迭代器输枯,里面存儲了當(dāng)前迭代的塊哈希(currentHash)和數(shù)據(jù)庫的連接(db)。通過 db占贫,迭代器邏輯上被附屬到一個區(qū)塊鏈上(這里的區(qū)塊鏈指的是存儲了一個數(shù)據(jù)庫連接的 Blockchain 實例)桃熄,并且通過 Blockchain 方法進(jìn)行創(chuàng)建:

func (bc *Blockchain) Iterator() *BlockchainIterator {
    bci := &BlockchainIterator{bc.tip, bc.db}

    return bci
}

注意,迭代器的初始狀態(tài)為鏈中的 tip型奥,因此區(qū)塊將從頭到尾瞳收,也就是從最新的到最舊的進(jìn)行獲取。實際上厢汹,選擇一個 tip 就是意味著給一條鏈“投票”螟深。一條鏈可能有多個分支,最長的那條鏈會被認(rèn)為是主分支坑匠。在獲得一個 tip (可以是鏈中的任意一個塊)之后血崭,我們就可以重新構(gòu)造整條鏈,找到它的長度和需要構(gòu)建它的工作。這同樣也意味著夹纫,一個 tip 也就是區(qū)塊鏈的一種標(biāo)識符咽瓷。

BlockchainIterator 只會做一件事情:返回鏈中的下一個塊。

func (i *BlockchainIterator) Next() *Block {
    var block *Block

    err := i.db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        encodedBlock := b.Get(i.currentHash)
        block = DeserializeBlock(encodedBlock)

        return nil
    })

    i.currentHash = block.PrevBlockHash

    return block
}

這就是數(shù)據(jù)庫部分的內(nèi)容了舰讹!

CLI

到目前為止茅姜,我們的實現(xiàn)還沒有提供一個與程序交互的接口:目前只是在 main 函數(shù)中簡單執(zhí)行了 NewBlockchainbc.AddBlock 。是時候改變了月匣!現(xiàn)在我們想要擁有這些命令:

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain

所有命令行相關(guān)的操作都會通過 CLI 結(jié)構(gòu)進(jìn)行處理:

type CLI struct {
    bc *Blockchain
}

它的 “入口” 是 Run 函數(shù):

func (cli *CLI) Run() {
    cli.validateArgs()

    addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
    printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)

    addBlockData := addBlockCmd.String("data", "", "Block data")

    switch os.Args[1] {
    case "addblock":
        err := addBlockCmd.Parse(os.Args[2:])
    case "printchain":
        err := printChainCmd.Parse(os.Args[2:])
    default:
        cli.printUsage()
        os.Exit(1)
    }

    if addBlockCmd.Parsed() {
        if *addBlockData == "" {
            addBlockCmd.Usage()
            os.Exit(1)
        }
        cli.addBlock(*addBlockData)
    }

    if printChainCmd.Parsed() {
        cli.printChain()
    }
}

我們會使用標(biāo)準(zhǔn)庫里面的 flag 包來解析命令行參數(shù):

addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")

首先钻洒,我們創(chuàng)建兩個子命令: addblockprintchain, 然后給 addblock 添加 -data 標(biāo)志。printchain 沒有任何標(biāo)志锄开。

switch os.Args[1] {
case "addblock":
    err := addBlockCmd.Parse(os.Args[2:])
case "printchain":
    err := printChainCmd.Parse(os.Args[2:])
default:
    cli.printUsage()
    os.Exit(1)
}

然后素标,我們檢查用戶提供的命令,解析相關(guān)的 flag 子命令:

if addBlockCmd.Parsed() {
    if *addBlockData == "" {
        addBlockCmd.Usage()
        os.Exit(1)
    }
    cli.addBlock(*addBlockData)
}

if printChainCmd.Parsed() {
    cli.printChain()
}

接著檢查解析是哪一個子命令萍悴,并調(diào)用相關(guān)函數(shù):

func (cli *CLI) addBlock(data string) {
    cli.bc.AddBlock(data)
    fmt.Println("Success!")
}

func (cli *CLI) printChain() {
    bci := cli.bc.Iterator()

    for {
        block := bci.Next()

        fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
        fmt.Printf("Data: %s\n", block.Data)
        fmt.Printf("Hash: %x\n", block.Hash)
        pow := NewProofOfWork(block)
        fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
        fmt.Println()

        if len(block.PrevBlockHash) == 0 {
            break
        }
    }
}

這部分內(nèi)容跟之前的很像头遭,唯一的區(qū)別是我們現(xiàn)在使用的是 BlockchainIterator 對區(qū)塊鏈中的區(qū)塊進(jìn)行迭代:

記得不要忘了對 main 函數(shù)作出相應(yīng)的修改:

func main() {
    bc := NewBlockchain()
    defer bc.db.Close()

    cli := CLI{bc}
    cli.Run()
}

注意,無論提供什么命令行參數(shù)癣诱,都會創(chuàng)建一個新的鏈计维。

這就是今天的所有內(nèi)容了! 來看一下是不是如期工作:

$ blockchain_go printchain
No existing blockchain found. Creating a new one...
Mining the block containing "Genesis Block"
000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b

Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true

$ blockchain_go addblock -data "Send 1 BTC to Ivan"
Mining the block containing "Send 1 BTC to Ivan"
000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13

Success!

$ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee"
Mining the block containing "Pay 0.31337 BTC for a coffee"
000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148

Success!

$ blockchain_go printchain
Prev. hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Data: Pay 0.31337 BTC for a coffee
Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
PoW: true

Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Data: Send 1 BTC to Ivan
Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
PoW: true

Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true
test

總結(jié)

在下篇文章中,我們將會實現(xiàn)地址撕予,錢包鲫惶,(可能實現(xiàn))交易。盡請收聽实抡!

鏈接

本文源碼:part_3

原文:Building Blockchain in Go. Part 3: Persistence and CLI

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末欠母,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子澜术,更是在濱河造成了極大的恐慌艺蝴,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸟废,死亡現(xiàn)場離奇詭異猜敢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)盒延,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門缩擂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人添寺,你說我怎么就攤上這事胯盯。” “怎么了计露?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵博脑,是天一觀的道長憎乙。 經(jīng)常有香客問我,道長叉趣,這世上最難降的妖魔是什么泞边? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮疗杉,結(jié)果婚禮上阵谚,老公的妹妹穿的比我還像新娘。我一直安慰自己烟具,他們只是感情好梢什,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著朝聋,像睡著了一般嗡午。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上玖翅,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天翼馆,我揣著相機(jī)與錄音,去河邊找鬼金度。 笑死,一個胖子當(dāng)著我的面吹牛严沥,可吹牛的內(nèi)容都是我干的猜极。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼消玄,長吁一口氣:“原來是場噩夢啊……” “哼跟伏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起翩瓜,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤受扳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后兔跌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體勘高,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年坟桅,在試婚紗的時候發(fā)現(xiàn)自己被綠了华望。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡仅乓,死狀恐怖赖舟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情夸楣,我是刑警寧澤宾抓,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布子漩,位于F島的核電站,受9級特大地震影響石洗,放射性物質(zhì)發(fā)生泄漏幢泼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一劲腿、第九天 我趴在偏房一處隱蔽的房頂上張望旭绒。 院中可真熱鬧,春花似錦焦人、人聲如沸挥吵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忽匈。三九已至,卻和暖如春矿辽,著一層夾襖步出監(jiān)牢的瞬間丹允,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工袋倔, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留雕蔽,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓宾娜,卻偏偏與公主長得像批狐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子前塔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

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