在Go中構(gòu)建區(qū)塊鏈瑰钮。 第3部分:持久性和CLI

介紹

到目前為止,我們已經(jīng)建立了一個(gè)工作證明系統(tǒng)的區(qū)塊鏈冒滩,這使得挖掘成為可能。 我們的實(shí)現(xiàn)越來越接近功能完整的區(qū)塊鏈飞涂,但它仍然缺乏一些重要功能。 今天將開始在數(shù)據(jù)庫中存儲區(qū)塊鏈祈搜,之后我們將制作一個(gè)簡單的命令行界面來執(zhí)行區(qū)塊鏈操作较店。 其本質(zhì)上,區(qū)塊鏈?zhǔn)且粋€(gè)分布式數(shù)據(jù)庫容燕。 現(xiàn)在我們將省略“分布式”部分梁呈,并專注于“數(shù)據(jù)庫”部分。

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

目前蘸秘,我們的實(shí)施中沒有數(shù)據(jù)庫; 相反官卡,我們每次運(yùn)行程序時(shí)都會創(chuàng)建塊并將它們存儲在內(nèi)存中。 我們不能重復(fù)使用區(qū)塊鏈醋虏,我們無法與其他人共享寻咒,因此我們需要將其存儲在磁盤上。

我們需要哪個(gè)數(shù)據(jù)庫颈嚼? 其實(shí)毛秘,他們中的任何一個(gè)。 在[最初的比特幣文件中] ,關(guān)于使用某個(gè)數(shù)據(jù)庫沒有任何說法叫挟,因此開發(fā)人員需要使用哪個(gè)DB艰匙。 [Bitcoin Core]最初由Satoshi Nakamoto發(fā)布,目前是比特幣的參考實(shí)現(xiàn)抹恳,它使用[LevelDB](盡管它僅在2012年引入客戶端)员凝。 我們將使用...

BoltDB

因?yàn)椋?/p>

它簡單而簡約。
它在Go中實(shí)現(xiàn)奋献。
它不需要運(yùn)行服務(wù)器健霹。
它允許構(gòu)建我們想要的數(shù)據(jù)結(jié)構(gòu)。
從[Github上]

Bolt是一個(gè)純粹的Go鍵/價(jià)值商店秽荞,受到了Howard Chu的LMDB項(xiàng)目的啟發(fā)骤公。 該項(xiàng)目的目標(biāo)是為不需要完整數(shù)據(jù)庫服務(wù)器(如Postgres或MySQL)的項(xiàng)目提供一個(gè)簡單,快速且可靠的數(shù)據(jù)庫扬跋。

由于Bolt旨在用作這種低級功能阶捆,因此簡單性是關(guān)鍵。 該API將很小钦听,只專注于獲取值和設(shè)置值洒试。 而已。

聽起來非常適合我們的需求朴上! 讓我們花一分鐘審查一下垒棋。

BoltDB是一個(gè)鍵/值存儲,這意味著沒有像SQL RDBMS(MySQL痪宰,PostgreSQL等)那樣的表??叼架,沒有行,沒有列衣撬。 相反乖订,數(shù)據(jù)存儲為鍵值對(如Golang地圖中)。 鍵值對存儲在桶中具练,用于對類似的對進(jìn)行分組(這與RDBMS中的表類似)乍构。 因此,為了獲得價(jià)值扛点,你需要知道一個(gè)水桶和一把鑰匙哥遮。

關(guān)于BoltDB的一個(gè)重要的事情是沒有數(shù)據(jù)類型:鍵和值是字節(jié)數(shù)組。 由于我們將Go結(jié)構(gòu)(特別是Block )存儲在其中陵究,因此我們需要序列化它們眠饮,即實(shí)現(xiàn)將Go結(jié)構(gòu)轉(zhuǎn)換為字節(jié)數(shù)組并將其從字節(jié)數(shù)組恢復(fù)的機(jī)制。 我們將為此使用[編碼/ gob]铜邮,但也可以使用JSON 君仆, XMLProtocol Buffers等。 我們使用encoding/gob因?yàn)樗芎唵畏翟郏⑶沂菢?biāo)準(zhǔn)Go庫的一部分钥庇。

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

在開始實(shí)施持久性邏輯之前,我們首先需要決定如何將數(shù)據(jù)存儲在數(shù)據(jù)庫中咖摹。 為此评姨,我們將介紹比特幣核心的做法。

簡而言之萤晴,Bitcoin Core使用兩個(gè)“存儲桶”來存儲數(shù)據(jù):

  1. blocks存儲描述鏈中所有blocks元數(shù)據(jù)吐句。
  2. chainstate存儲chainstate的狀態(tài),目前所有鏈接都是未使用的事務(wù)輸出和一些元數(shù)據(jù)店读。

另外嗦枢,塊在磁盤上作為單獨(dú)的文件存儲。 這是為了達(dá)到性能目的而完成的:讀取單個(gè)塊不需要將全部(或部分)全部加載到內(nèi)存中屯断。 我們不會執(zhí)行這個(gè)文虏。

blockskey -> value對是:

  1. 'b' + 32-byte block hash -> block index record
  2. 'f' + 4-byte file number -> file information record
  3. 'l' -> 4-byte file number: the last block file number used
  4. 'R' -> 1-byte boolean: whether we're in the process of reindexing
  5. 'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off
  6. 't' + 32-byte transaction hash -> transaction index record

chainstate 殖演, key -> value對是:

  1. 'c' + 32-byte transaction hash -> unspent transaction output record for that transaction
  2. 'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs

(詳細(xì)解釋可以在[這里]找到)

由于我們還沒有交易氧秘,因此我們只會blocks存貨桶。 另外趴久,如上所述丸相,我們將整個(gè)DB存儲為單個(gè)文件,而不將塊存儲在單獨(dú)的文件中彼棍。 所以我們不需要任何與文件編號有關(guān)的東西灭忠。 因此,這些是我們將使用的key -> value對:

  1. 32-byte block-hash -> Block structure (serialized)
  2. 'l' -> the hash of the last block in a chain

這就是我們開始實(shí)施持久性機(jī)制所需要知道的座硕。

序列化

如前所述弛作,BoltDB中的值只能是[]byte類型,并且我們希望將Block結(jié)構(gòu)存儲在數(shù)據(jù)庫中坎吻。 我們將使用[編碼/ gob]來序列化結(jié)構(gòu)缆蝉。

讓我們來實(shí)現(xiàn)BlockSerialize方法(為簡潔起見省略了錯(cuò)誤處理):

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

    err := encoder.Encode(b)

    return result.Bytes()
}

這篇文章很簡單:首先宇葱,我們聲明一個(gè)緩沖區(qū)瘦真,它將存儲序列化的數(shù)據(jù); 然后我們初始化一個(gè)gob編碼器并編碼該塊; 結(jié)果以字節(jié)數(shù)組的形式返回。

接下來黍瞧,我們需要一個(gè)反序列化函數(shù)诸尽,它將接收一個(gè)字節(jié)數(shù)組作為輸入并返回一個(gè)Block 。 這不是一個(gè)方法印颤,而是一個(gè)獨(dú)立的功能:

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

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

    return &block
}

這就是序列化您机!

堅(jiān)持

我們從NewBlockchain函數(shù)開始。 目前,它創(chuàng)建了一個(gè)新的Blockchain實(shí)例际看,并為其添加了生成塊咸产。 我們希望它做的是:

打開一個(gè)數(shù)據(jù)庫文件。
檢查是否存在區(qū)塊鏈仲闽。
如果有區(qū)塊鏈:
創(chuàng)建一個(gè)新的Blockchain實(shí)例脑溢。
將Blockchain實(shí)例的頂端設(shè)置為存儲在數(shù)據(jù)庫中的最后一個(gè)塊哈希。
如果不存在區(qū)塊鏈:
創(chuàng)建起始塊赖欣。
存儲在數(shù)據(jù)庫中屑彻。
將創(chuàng)建塊的散列保存為最后一個(gè)塊散列。
創(chuàng)建一個(gè)新的Blockchain實(shí)例顶吮,其尖端指向創(chuàng)世區(qū)塊社牲。
在代碼中,它看起來像這樣:

 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)方式搏恤。 注意,如果沒有這樣的文件让禀,它不會返回錯(cuò)誤挑社。

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

在BoltDB中,數(shù)據(jù)庫操作在事務(wù)中運(yùn)行巡揍。 有兩種類型的事務(wù):只讀和讀寫痛阻。 在這里,我們打開一個(gè)讀寫事務(wù)( db.Update(...) )腮敌,因?yàn)槲覀兿M麑⑸蓧K放在數(shù)據(jù)庫中阱当。

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

這是該功能的核心。 在這里糜工,我們獲得存儲塊的桶:如果它存在弊添,我們從它讀取l鍵; 如果它不存在,我們生成生成塊捌木,創(chuàng)建桶油坝,將塊保存到其中,并更新存儲鏈的最后塊哈希的l密鑰刨裆。

另外澈圈,請注意創(chuàng)建Blockchain的新方法:

  bc := Blockchain{tip, db}

我們不再存儲所有的塊,而只是存儲鏈的頂端帆啃。 另外瞬女,我們存儲一個(gè)數(shù)據(jù)庫連接,因?yàn)槲覀兿氪蜷_它一次努潘,并在程序運(yùn)行時(shí)保持打開狀態(tài)诽偷。 因此坤学, Blockchain結(jié)構(gòu)現(xiàn)在看起來像這樣:

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

接下來我們要更新的是AddBlock方法:現(xiàn)在向鏈中添加塊并不像將元素添加到數(shù)組中那么容易。 從現(xiàn)在開始报慕,我們將塊存儲在DB中:

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

讓我們逐一回顧一下:

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ù)庫中獲取最后一個(gè)塊哈希來使用它來挖掘一個(gè)新的塊哈希眠冈。

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

在挖掘新塊之后略号,我們將其序列化表示保存到數(shù)據(jù)塊中并更新l密鑰,該密鑰現(xiàn)在存儲新塊的哈希洋闽。

完成玄柠! 這并不難,是嗎诫舅?

檢查區(qū)塊鏈

所有新塊現(xiàn)在都保存在數(shù)據(jù)庫中羽利,因此我們可以重新打開區(qū)塊鏈并為其添加新塊。 但是在實(shí)現(xiàn)這個(gè)之后刊懈,我們失去了一個(gè)很好的特性:我們不能再打印出區(qū)塊鏈塊这弧,因?yàn)槲覀儾辉賹K存儲在數(shù)組中。 讓我們來修復(fù)這個(gè)缺陷虚汛!

BoltDB允許迭代桶中的所有鍵匾浪,但鍵以字節(jié)排序的順序存儲,我們希望塊按照它們在區(qū)塊鏈中的順序進(jìn)行打印卷哩。 另外蛋辈,因?yàn)槲覀儾幌雽⑺袎K加載到內(nèi)存中(我們的區(qū)塊鏈數(shù)據(jù)庫可能很大,或者我們假裝它可以)将谊,我們將逐個(gè)讀取它們冷溶。 為此,我們需要一個(gè)區(qū)塊鏈迭代器:

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

}

每次我們想要遍歷區(qū)塊鏈中的塊時(shí)尊浓,都會創(chuàng)建一個(gè)迭代器逞频,它將存儲當(dāng)前迭代的塊散列和到數(shù)據(jù)庫的連接。 由于后者栋齿,迭代器在邏輯上連接到Blockchain鏈(它是一個(gè)存儲數(shù)據(jù)庫連接的Blockchain實(shí)例)苗胀,因此,它是在Blockchain方法中創(chuàng)建的:

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

    return bci
}

請注意瓦堵,迭代器最初指向區(qū)塊鏈的頂端基协,因此塊將從上到下,從最新到最舊獲取谷丸。 事實(shí)上堡掏, 選擇提示意味著區(qū)塊鏈的“投票” 应结。 區(qū)塊鏈可以有多個(gè)分支刨疼,并且它們中被認(rèn)為是最長的分支泉唁。 獲得小費(fèi)后(可以是區(qū)塊鏈中的任何區(qū)塊),我們可以重新構(gòu)建整個(gè)區(qū)塊鏈揩慕,并查找其長度以及構(gòu)建區(qū)塊鏈所需的工作亭畜。 這個(gè)事實(shí)也意味著提示是一種區(qū)塊鏈的標(biāo)識符寺鸥。

BlockchainIterator只會做一件事:它會從區(qū)塊鏈返回下一個(gè)區(qū)塊扫尺。

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ù)庫部分!

CLI

直到現(xiàn)在我們的實(shí)現(xiàn)還沒有提供任何接口來與程序交互:我們只是在main函數(shù)中執(zhí)行了NewBlockchain 撇叁, bc.AddBlock 蜗搔。 時(shí)間來改善這一點(diǎn)劲藐! 我們想要這些命令:

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain

所有與命令行相關(guān)的操作都將由CLI結(jié)構(gòu)處理:

type CLI struct {
    bc *Blockchain
}

它的“入口點(diǎn)”是Run功能:

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)建兩個(gè)子命令聘芜, addblock和printchain ,然后printchain添加-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()
}

接下來我們檢查哪些子命令被解析并運(yùn)行相關(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
    }
}
}

這件作品與我們之前的作品非常相似瞎饲。 唯一的區(qū)別是我們現(xiàn)在使用BlockchainIterator遍歷區(qū)塊鏈中的塊。

另外我們不要忘記相應(yīng)地修改main函數(shù):

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

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

請注意炼绘,無論提供什么命令行參數(shù)嗅战,都會創(chuàng)建新的Blockchain 。

就是這樣俺亮! 讓我們來檢查一切是否按預(yù)期工作:

$ 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

(啤酒的聲音可以打開)

結(jié)論
下次我們將實(shí)施地址仗哨,錢包和(可能)交易。 敬請期待铅辞!

Links

  1. Full source codes
  2. Bitcoin Core Data Storage
  3. boltdb
  4. encoding/gob
  5. flag
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末厌漂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子斟珊,更是在濱河造成了極大的恐慌苇倡,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件囤踩,死亡現(xiàn)場離奇詭異旨椒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)堵漱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門综慎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人勤庐,你說我怎么就攤上這事示惊『酶郏” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵米罚,是天一觀的道長钧汹。 經(jīng)常有香客問我,道長录择,這世上最難降的妖魔是什么拔莱? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮隘竭,結(jié)果婚禮上塘秦,老公的妹妹穿的比我還像新娘。我一直安慰自己动看,他們只是感情好嗤形,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著弧圆,像睡著了一般赋兵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搔预,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天霹期,我揣著相機(jī)與錄音,去河邊找鬼拯田。 笑死历造,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的船庇。 我是一名探鬼主播吭产,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鸭轮!你這毒婦竟也來了臣淤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤窃爷,失蹤者是張志新(化名)和其女友劉穎邑蒋,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體按厘,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡医吊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了逮京。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卿堂。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖懒棉,靈堂內(nèi)的尸體忽然破棺而出草描,到底是詐尸還是另有隱情览绿,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布陶珠,位于F島的核電站,受9級特大地震影響享钞,放射性物質(zhì)發(fā)生泄漏揍诽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一栗竖、第九天 我趴在偏房一處隱蔽的房頂上張望暑脆。 院中可真熱鬧,春花似錦狐肢、人聲如沸添吗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碟联。三九已至,卻和暖如春僵腺,著一層夾襖步出監(jiān)牢的瞬間鲤孵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工辰如, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留普监,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓琉兜,卻偏偏與公主長得像凯正,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子豌蟋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

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