Go-ethereum 源碼解析之 consensus/clique/snapshot.go

Go-ethereum 源碼解析之 consensus/clique/snapshot.go

package clique

import (
    "bytes"
    "encoding/json"
    "sort"

    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/ethdb"
    "github.com/ethereum/go-ethereum/params"
    lru "github.com/hashicorp/golang-lru"
)

// Vote represents a single vote that an authorized signer made to modify the
// list of authorizations.
type Vote struct {
    Signer    common.Address `json:"signer"`    // Authorized signer that cast this vote
    Block     uint64         `json:"block"`     // Block number the vote was cast in (expire old votes)
    Address   common.Address `json:"address"`   // Account being voted on to change its authorization
    Authorize bool           `json:"authorize"` // Whether to authorize or deauthorize the voted account
}

// Tally is a simple vote tally to keep the current score of votes. Votes that
// go against the proposal aren't counted since it's equivalent to not voting.
type Tally struct {
    Authorize bool `json:"authorize"` // Whether the vote is about authorizing or kicking someone
    Votes     int  `json:"votes"`     // Number of votes until now wanting to pass the proposal
}

// Snapshot is the state of the authorization voting at a given point in time.
type Snapshot struct {
    config   *params.CliqueConfig // Consensus engine parameters to fine tune behavior
    sigcache *lru.ARCCache        // Cache of recent block signatures to speed up ecrecover

    Number  uint64                      `json:"number"`  // Block number where the snapshot was created
    Hash    common.Hash                 `json:"hash"`    // Block hash where the snapshot was created
    Signers map[common.Address]struct{} `json:"signers"` // Set of authorized signers at this moment
    Recents map[uint64]common.Address   `json:"recents"` // Set of recent signers for spam protections
    Votes   []*Vote                     `json:"votes"`   // List of votes cast in chronological order
    Tally   map[common.Address]Tally    `json:"tally"`   // Current vote tally to avoid recalculating
}

// signers implements the sort interface to allow sorting a list of addresses
type signers []common.Address

func (s signers) Len() int           { return len(s) }
func (s signers) Less(i, j int) bool { return bytes.Compare(s[i][:], s[j][:]) < 0 }
func (s signers) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

// newSnapshot creates a new snapshot with the specified startup parameters. This
// method does not initialize the set of recent signers, so only ever use if for
// the genesis block.
func newSnapshot(config *params.CliqueConfig, sigcache *lru.ARCCache, number uint64, hash common.Hash, signers []common.Address) *Snapshot {
    snap := &Snapshot{
        config:   config,
        sigcache: sigcache,
        Number:   number,
        Hash:     hash,
        Signers:  make(map[common.Address]struct{}),
        Recents:  make(map[uint64]common.Address),
        Tally:    make(map[common.Address]Tally),
    }
    for _, signer := range signers {
        snap.Signers[signer] = struct{}{}
    }
    return snap
}

Appendix A. 總體批注

文件 clique/snapshot.go 主要是用于描述 Clique 共識算法中關于授權簽名者列表生成的快照信息,以及授權簽名者對給定區(qū)塊頭列表如何進行具體簽名的規(guī)則。

假設授權簽名者列表的長度為 K,當前進行投票的區(qū)塊編號為 N,給定區(qū)塊頭中的投票簽名者在有序授權簽名者列表中的偏移為 P,偏移從 0 開始。

快照中包含的主要信息有:

  • 創(chuàng)建快照時的區(qū)塊編號
  • 創(chuàng)建快照時的區(qū)塊哈希
  • 授權簽名者集合
  • 最近 K/2 + 1 個區(qū)塊中各區(qū)塊編號對應的簽名者集合
  • 按區(qū)塊編號順序投票的投票列表
  • 以及各被投票簽名者的得票計數(shù)器。

授權簽名者的具體簽名規(guī)則:

  • 待應用簽名的區(qū)塊頭列表需要滿足要求:區(qū)塊的編號是連續(xù)的。
  • K 個簽名者各自在最近連續(xù)的 K/2 + 1 個區(qū)塊最多只能投出一票。
  • 第 P 個簽名者只能在滿足 N % K == P 條件的區(qū)塊中進行投票。
  • 對于一個投票,得票數(shù)需要超過 K/2,不包括 K/2。

??? 第 1 個疑問:大多數(shù)時候在區(qū)塊頭中并不會進行投票,而區(qū)塊頭列表又需要滿足連續(xù)性這個條件,但是看代碼中對于不包含投票的區(qū)塊頭并沒有直接過濾的操作。

??? 第 2 個疑問:根據(jù)授權簽名者的具體簽名規(guī)則,在知道 K 的時候,能夠推斷出在區(qū)塊 N 中進行投票的簽名者為 P。這在 PoA 聯(lián)盟鏈中會不會導致安全漏洞。

!!! 一個 BUG:在投票解除授權簽名者時,存在一個問題。當授權簽名者列表中只剩下一個簽名者,且該簽名者投票解除自己的授權時,會觸發(fā)此問題,導致授權簽名者列表為空,引起之后用授權簽名者列表長度作分母時的代碼報除 0 錯誤。
- 真正有問題的代碼,具體代碼見方法 Snapshot.apply() 中的 delete(snap.Signers, header.Coinbase)。
- 觸發(fā)問題的代碼,具體代碼見方法 Snapshot.inturn() 中的 return (number % uint64(len(signers))) == uint64(offset)

定義了多種數(shù)據(jù)結(jié)構(gòu),如:

  • 數(shù)據(jù)結(jié)構(gòu) Vote 用于描述一次具體的投票信息。
  • 數(shù)據(jù)結(jié)構(gòu) Tally 用于描述一個簡單的投票計數(shù)器。
  • 數(shù)據(jù)結(jié)構(gòu) Snapshot 用于描述指定時間點的授權投票狀態(tài)。
  • 數(shù)據(jù)結(jié)構(gòu) signers 用于描述授權簽名者列表的封裝器,并實現(xiàn)了排序接口。數(shù)據(jù)結(jié)構(gòu) singers 支持對授權簽名者列表進行升序排序,因此可以計算出給定簽名者在整個授權簽名者列表的有序偏移 P。

1. type Vote struct

數(shù)據(jù)結(jié)構(gòu) Vote 表示授權簽名者為了修改授權列表而進行的一次投票。

  • Signer common.Address: 投票的授權簽名者
  • Block uint6: 投票的區(qū)塊編號(投票過期)
  • Address common.Address: 被投票的帳戶,以更改其授權
  • Authorize bool: 表示是否授權或取消對已投票帳戶的授權

2. type Tally struct

數(shù)據(jù)結(jié)構(gòu) Tally 是一個簡單的投票計數(shù)器,以保持當前的投票得分。投票反對該提案不計算在內(nèi),因為它等同于不投票。

  • Authorize bool: 投票是關于授權還是踢某人
  • Votes int: 到目前為止希望通過提案的投票數(shù)

3. type Snapshot struct

數(shù)據(jù)結(jié)構(gòu) Snapshot 表示指定時間點的授權投票狀態(tài)。

  • config *params.CliqueConfig: 共識引擎參數(shù)以微調(diào)行為

  • sigcache *lru.ARCCache: 緩存最近的塊簽名以加速函數(shù) ecrecover()

  • Number uint64: 創(chuàng)建快照的區(qū)塊編號

  • Hash common.Hash: 創(chuàng)建快照的區(qū)塊哈希

  • Signers map[common.Address]struct{}: 這一刻的授權簽名者集合

  • Recents map[uint64]common.Address: 一組最近的簽名者集,用于防止 spam 攻擊。分別記錄最近 k/2 + 1 次的區(qū)塊編號對應的簽名者。

  • Votes []*Vote: 按區(qū)塊編號順序投票的投票列表

  • Tally map[common.Address]Tally: 目前的投票計數(shù)器,以避免重新計算

  • 通過構(gòu)造函數(shù)
    newSnapshot() 使用指定的啟動參數(shù)創(chuàng)建新快照。這種方法不會初始化最近的簽名者集,所以只能用于創(chuàng)世塊。

  • 通過函數(shù) loadSnapshot() 從數(shù)據(jù)庫加載已經(jīng)存在的快照。

  • 通過方法 store() 將快照插入數(shù)據(jù)庫。

  • 通過方法 copy() 會創(chuàng)建快照的深層副本,但不會創(chuàng)建單獨的投票。

  • 通過方法 validVote() 返回在給定的快照上下文中投出的特定投票是否有意義(例如,不要嘗試添加已經(jīng)授權的簽名者)。

  • 通過方法 cast() 往投票計數(shù)器 Snapshot.tally 中增加新的投票。

  • 通過方法 uncast() 從投票計數(shù)器 Snapshot.tally 中移除之前的一次投票。

  • 通過方法 apply() 通過將給定的區(qū)塊頭列表應用于原始的快照來生成新的授權快照。

  • 通過方法 signers() 按升序返回授權簽名者列表。

  • 通過方法 inturn() 返回簽名者在給定區(qū)塊高度是否是 in-turn 的。

4. type signers []common.Address

封裝器 signers 實現(xiàn)了排序接口,以允許排序地址列表。

  • 通過方法 Len() 返回列表中元素的個數(shù)。
  • 通過方法 Less() 比較列表中第 i 個元素是否比第 j 個元素的小,如果是返回 true。
  • 通過方法 Swap() 交換列表中第 i 個元素和第 j 個元素。

Appendix B. 詳細批注

1. type Vote struct

數(shù)據(jù)結(jié)構(gòu) Vote 表示授權簽名者為了修改授權列表而進行的一次投票。

  • Signer common.Address: 投票的授權簽名者
  • Block uint6: 投票的區(qū)塊編號(投票過期)
  • Address common.Address: 被投票的帳戶,以更改其授權
  • Authorize bool: 表示是否授權或取消對已投票帳戶的授權

2. type Tally struct

數(shù)據(jù)結(jié)構(gòu) Tally 是一個簡單的投票計數(shù)器,以保持當前的投票得分。投票反對該提案不計算在內(nèi),因為它等同于不投票。

  • Authorize bool: 投票是關于授權還是踢某人
  • Votes int: 到目前為止希望通過提案的投票數(shù)

3. type Snapshot struct

數(shù)據(jù)結(jié)構(gòu) Snapshot 表示指定時間點的授權投票狀態(tài)。

  • config *params.CliqueConfig: 共識引擎參數(shù)以微調(diào)行為

  • sigcache *lru.ARCCache: 緩存最近的塊簽名以加速函數(shù) ecrecover()

  • Number uint64: 創(chuàng)建快照的區(qū)塊編號

  • Hash common.Hash: 創(chuàng)建快照的區(qū)塊哈希

  • Signers map[common.Address]struct{}: 這一刻的授權簽名者集合

  • Recents map[uint64]common.Address: 一組最近的簽名者集,用于防止 spam 攻擊。分別記錄最近 k/2 + 1 次的區(qū)塊編號對應的簽名者。

  • Votes []*Vote: 按區(qū)塊編號順序投票的投票列表

  • Tally map[common.Address]Tally: 目前的投票計數(shù)器,以避免重新計算

1. func newSnapshot(config *params.CliqueConfig, sigcache *lru.ARCCache, number uint64, hash common.Hash, signers []common.Address) *Snapshot

構(gòu)造函數(shù)
newSnapshot() 使用指定的啟動參數(shù)創(chuàng)建新快照。這種方法不會初始化最近的簽名者集,所以只能用于創(chuàng)世塊。

2. func loadSnapshot(config *params.CliqueConfig, sigcache lru.ARCCache, db ethdb.Database, hash common.Hash) (Snapshot, error)

函數(shù) loadSnapshot() 從數(shù)據(jù)庫加載已經(jīng)存在的快照。

主要的實現(xiàn)細節(jié)如下:

  • 調(diào)用方法 db.Get() 從數(shù)據(jù)庫加載 JSON 數(shù)據(jù)流
  • 調(diào)用方法 json.Unmarshal() 從 JSON 數(shù)據(jù)流中解碼出對象 clique.Snapshot
  • 與方法 Snapshot.store() 的功能相反。

3. func (s *Snapshot) store(db ethdb.Database) error

方法 store() 將快照插入數(shù)據(jù)庫。

主要的實現(xiàn)細節(jié)如下:

  • 調(diào)用方法 json.Marshal() 將對象 clique.Snapshot 編碼成 JSON 數(shù)據(jù)流。
  • 調(diào)用方法 db.Put() 將 JSON 數(shù)據(jù)流插入數(shù)據(jù)庫。
  • 與函數(shù) loadSnapshot() 的功能相反。

4. func (s *Snapshot) copy() *Snapshot

方法 copy() 會創(chuàng)建快照的深層副本,但不會創(chuàng)建單獨的投票。

5. func (s *Snapshot) validVote(address common.Address, authorize bool) bool

方法 validVote() 返回在給定的快照上下文中投出的特定投票是否有意義(例如,不要嘗試添加已經(jīng)授權的簽名者)。

主要的實現(xiàn)細節(jié)如下:

  • 當 authorize 為 true 時,則 address 應該不存在于 Snapshot.Signers;且當 authorize 為 false 時,則 address 應該存在于 Snapshot.Signers。這兩種情況都是有效的投票,否則為無效的投票。
  • 也就是當投出剔除授權簽名者,該簽名者應該存在于授權簽名者列表。當投出新增授權簽名者時,該簽名者應該不存在于授權簽名者列表。
  • 判定算法有點繞
    • return (signer && !authorize) || (!signer && authorize)
func (s *Snapshot) validVote(address common.Address, authorize bool) bool {
    _, signer := s.Signers[address]
    return (signer && !authorize) || (!signer && authorize)
}

6. func (s *Snapshot) cast(address common.Address, authorize bool) bool

方法 cast() 往投票計數(shù)器 Snapshot.tally 中增加新的投票。

主要的實現(xiàn)細節(jié)如下:

  • 調(diào)用方法 Snapshot.validVote() 驗證投票的有效性。
  • 需要考慮對指定地址的投票是全新的,還是只是增加得票數(shù)即可。

7. func (s *Snapshot) uncast(address common.Address, authorize bool) bool

方法 uncast() 從投票計數(shù)器 Snapshot.tally 中移除之前的一次投票。

主要的實現(xiàn)細節(jié)如下:

  • 需要確保此次投票和之前的投票一致。
  • 返還投票時需要考慮返還后指定地址的得票數(shù)是否為 0.

8. func (s Snapshot) apply(headers []types.Header) (*Snapshot, error)

方法 apply() 通過將給定的區(qū)塊頭列表應用于原始的快照來生成新的授權快照。

主要的實現(xiàn)細節(jié)如下:

  • 如果 len(headers) == 0,則直接返回。允許傳入空 headers 以獲得更清晰的代碼。

  • 檢查區(qū)塊頭列表的完整性。即區(qū)塊頭列表中的區(qū)塊頭必須是連續(xù)的,且是根據(jù)區(qū)塊編號升序排序的。

  • 除了參數(shù) headers 必須是連續(xù)且升序之外,第一個區(qū)塊頭的區(qū)塊編號也必須是當前快照所處的區(qū)塊編號的下一個區(qū)塊, 即 headers[0].Number.Uint64() != s.Number+1。

  • 通過方法 Snapshot.copy() 創(chuàng)建要返回的新的快照,并在此新快照上依次應用參數(shù)區(qū)塊頭列表中的區(qū)塊頭 header。

    • 檢查當前區(qū)塊頭是否為檢查點區(qū)塊,如果是則清除所有的投票信息。
    • 從最近的簽名者列表(snap.Recents)中刪除最舊的簽名者以允許它再次簽名。
      • 具體規(guī)則為:Snapshot.Recents 最多只會記錄 K/2 + 1 個最近的簽名者簽名記錄,也就是簽名者在最近 K/2 + 1 個區(qū)塊中只能簽名一次。具體的計算規(guī)則是:假設當前區(qū)塊的編號為 N,會刪除 Snapshot.Recents 中第 N - (K/2 + 1) 個元素,之后 Snapshot.Recents 中的第 1 個元素為 N - (K/2 + 1) + 1,在 N - (K/2 + 1) + 1 和 N 之間存在 (N - (N - (K/2 + 1) + 1) + 1) = K/2 + 1。之所以是 number >= limit,這里 limit = K/2 + 1,是由于第 1 個區(qū)塊的編號為 0,由 0 到 limit - 1 正好包含 (limit - 1) - 0 + 1 = (K/2 + 1 - 1) - 0 + 1 = k/2 + 1 個區(qū)塊。
    • 調(diào)用函數(shù) ecrecover() 從區(qū)塊頭中恢復出簽名者 signer。
    • 檢查簽名者 signer 是否存在于授權簽名者列表(snap.Signers),不存在返回 clique.errUnauthorized。
    • 檢查簽名者 singer 是否在最近 K/2 + 1 個區(qū)塊中已經(jīng)簽名過,即是否已經(jīng)存在于最近的簽名者列表(snap.Recents)中。已經(jīng)簽名過則返回 clique.errUnauthorized。
    • 更新最近的簽名者列表(snap.Recents),snap.Recents[number] = signer。
    • 對于授權的區(qū)塊頭,丟棄簽名者以前的任何投票 vote。
      • 通過方法 snap.uncast(vote.Address, vote.Authorize) 從投票計數(shù)器(Snapshot.Tally)移除該投票。
      • 通過 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 從 Snapshot.Votes 中移除該投票。
    • 從區(qū)塊頭 types.Header.Nonce 中計算是授權(nonceAuthVote)還是解除授權(nonceDropVote)投票,無效 Nonce 值則返回 clique.errInvalidVote。
    • 通過方法 Snapshot.cast() 更新投票計數(shù)器(Snapshot.Tally)。如果成功,則往 snap.Votes 添加新的投票。
    • 如果區(qū)塊頭 header 中的投票被通過,則更新授權簽名者列表。一次投票被通過的條件是,得票數(shù)大于等于 K/2 + 1,其中 K 為授權簽名者個數(shù)。
      • 如果投票是授權簽名者,則 snap.Signers[header.Coinbase] = struct{}{}
      • 如果投票是解除授權簽名者,則:
        • delete(snap.Signers, header.Coinbase)。
        • 簽名者列表縮小,刪除任何剩余的最近的簽名者列表(snap.Recents)緩存,這個操作是為了維持與 K/2 + 1 相關的這個規(guī)則。
        • 丟棄授權簽名者以前的任何投票,即調(diào)用 snap.uncast 更新 snap.Votes,和通過 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 更新 snap.Votes。注意具體實現(xiàn)時的 i-- 操作,這是由于 snap.Votes 的長度已經(jīng)縮小了 1.
      • 丟棄剛剛更改的帳戶(header.coinbase)的所有先前投票
        • 通過 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) 修改 snap.Votes。同時注意與上述相似的 i-- 操作。
        • 通過 delete(snap.Tally, header.Coinbase) 直接從 snap.Tally 刪除 header.Coinbase 的整個計數(shù)器。
  • 更新當前快照創(chuàng)建時的區(qū)塊編號。即將原快照創(chuàng)建時的區(qū)塊編號加上參數(shù) headers 中 types.Header 的個數(shù),具體實現(xiàn)為 snap.Number += uint64(len(headers))

  • 更新當前快照創(chuàng)建時的區(qū)塊哈希。即參數(shù) headers 中最后一個 types.Header 的哈希。snap.Hash = headers[len(headers)-1].Hash()

9. func (s *Snapshot) signers() []common.Address

方法 signers() 按升序返回授權簽名者列表。

主要的實現(xiàn)細節(jié)如下:

  • 通過方法 sort.Sort() 按升序排序授權簽名者列表。

10. func (s *Snapshot) inturn(number uint64, signer common.Address) bool

方法 inturn() 返回簽名者在給定區(qū)塊高度是否是 in-turn 的。

這里可以理解 in-turn 為授權簽名者列表對于給定區(qū)塊判定采用哪個簽名者的規(guī)則。

假設區(qū)塊編號為 N,也就是區(qū)塊的高度為 N。授權簽名者列表的長度為 K。簽名者在授權簽名者列表中的順序為 P,從 0 開始偏移。則如果 (N % K) == P 就返回 true,表示 in-turn。

主要的實現(xiàn)細節(jié)如下:

  • 即實現(xiàn)上面的規(guī)則。

4. type signers []common.Address

封裝器 signers 實現(xiàn)了排序接口,以允許排序地址列表。

(1) func (s signers) Len() int

方法 Len() 返回列表中元素的個數(shù)。

(2) func (s signers) Less(i, j int) bool

方法 Less() 比較列表中第 i 個元素是否比第 j 個元素的小,如果是返回 true。

(3) func (s signers) Swap(i, j int)

方法 Swap() 交換列表中第 i 個元素和第 j 個元素。

Reference

  1. https://github.com/ethereum/go-ethereum/blob/master/consensus/clique/snapshot.go

Contributor

  1. Windstamp, https://github.com/windstamp
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

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