以太坊源码研读0x08 Miner模块

我们都知道从比特币开始,我们将打包出一个合法区块的节点叫做Miner(矿工),同时将这个过程叫做Mining(挖矿)。这个比喻是很贴切的,因为无论是Bitcoin还是Eth其代币数量都是有限的,就像地球上的黄金储备量,你从金矿挖出一点其储备就会少一点。

wiki关于挖矿的描述这里就不再赘述。我们直捣黄龙开撸源码!!!

Miner结构

// Miner creates blocks and searches for proof-of-work values.
type Miner struct {
    // 事件锁
    mux *event.TypeMux

    // 真正干活的人
    worker *worker
    // 矿工地址
    coinbase common.Address
    // 表示正在挖矿的状态
    mining   int32
    // Backend对象,Backend是一个自定义接口封装了所有挖矿所需方法
    eth      Backend
    // 共识引擎 以太坊有两种共识引擎ethash和clique
    engine   consensus.Engine

    // 是否能够开始挖矿
    canStart    int32 // can start indicates whether we can start the mining operation
    // 同步后是否应该开始挖矿
    shouldStart int32 // should start indicates whether we should start after sync
}

miner只是以太坊对外实现mining功能的开放类,真正干活的是worker。所以,继续深入看看worker的结构。

// worker is the main object which takes care of applying messages to the new state
type worker struct {
    // 链的配置属性
    config *params.ChainConfig
    // 前面提到的共识引擎
    engine consensus.Engine
    // 同步锁
    mu sync.Mutex

    // update loop
    mux          *event.TypeMux
    txsCh        chan core.NewTxsEvent
    txsSub       event.Subscription
    chainHeadCh  chan core.ChainHeadEvent
    chainHeadSub event.Subscription
    chainSideCh  chan core.ChainSideEvent
    chainSideSub event.Subscription
    wg           sync.WaitGroup

    // Agent的map集合
    agents map[Agent]struct{}
    recv   chan *Result

    eth     Backend
    chain   *core.BlockChain
    proc    core.Validator
    chainDb ethdb.Database

    coinbase common.Address
    extra    []byte

    currentMu sync.Mutex
    current   *Work

    snapshotMu    sync.RWMutex
    snapshotBlock *types.Block
    snapshotState *state.StateDB

    uncleMu        sync.Mutex
    possibleUncles map[common.Hash]*types.Block

    // 本地挖出的有待确认的区块
    unconfirmed *unconfirmedBlocks // set of locally mined blocks pending canonicalness confirmations

    // atomic status counters
    mining int32
    atWork int32
}

这里,我们来着重看一下params.ChainConfig这个结构,顾名思义,它是链的配置属性,其中定义了一些针对以太坊历史问题的相关配置。


// ChainConfig is stored in the database on a per block basis. This means
// that any network, identified by its genesis block, can have its own
// set of configuration options.
type ChainConfig struct {
    // 标识当前链,主键唯一id 也用来防止replay attack重放攻击
    ChainID *big.Int `json:"chainId"` // chainId identifies the current chain and is used for replay protection
    // 以太坊发展蓝图中的一个阶段,当前阶段为Homestead
    // 第一阶段为以太坊面世代号frontier,第二阶段为Homestead即当前阶段
    // 第三阶段为Metropolis(大都会),Metropolis又分为Byzantium(拜占庭硬分叉,引入新型零知识证明算法和pos共识),
    // 然后是constantinople(君士坦丁堡硬分叉,eth正是应用pow和pos混合链)
    // 第四阶段为Serenity(宁静),最终稳定版的以太坊
    HomesteadBlock *big.Int `json:"homesteadBlock,omitempty"` // Homestead switch block (nil = no fork, 0 = already homestead)

    // TheDao硬分叉切换,2017年6月18日应对DAO危机做出的调整
    DAOForkBlock   *big.Int `json:"daoForkBlock,omitempty"`   // TheDAO hard-fork switch block (nil = no fork)
    // 节点是否支持TheDao硬分叉
    DAOForkSupport bool     `json:"daoForkSupport,omitempty"` // Whether the nodes supports or opposes the DAO hard-fork

    // EIP150 implements the Gas price changes (https://github.com/ethereum/EIPs/issues/150)
    // eth改善方案硬分叉  没有硬分叉的置0
    EIP150Block *big.Int    `json:"eip150Block,omitempty"` // EIP150 HF block (nil = no fork)
    EIP150Hash  common.Hash `json:"eip150Hash,omitempty"`  // EIP150 HF hash (needed for header only clients as only gas pricing changed)

    EIP155Block *big.Int `json:"eip155Block,omitempty"` // EIP155 HF block
    EIP158Block *big.Int `json:"eip158Block,omitempty"` // EIP158 HF block

    ByzantiumBlock      *big.Int `json:"byzantiumBlock,omitempty"`      // Byzantium switch block (nil = no fork, 0 = already on byzantium)
    ConstantinopleBlock *big.Int `json:"constantinopleBlock,omitempty"` // Constantinople switch block (nil = no fork, 0 = already activated)

    // Various consensus engines
    Ethash *EthashConfig `json:"ethash,omitempty"`
    Clique *CliqueConfig `json:"clique,omitempty"`
}

接着在worker里有一个agent代理,他们的关系应该是这样的。一个miner有一个worker,一个worker又同时拥有多个agent。这里的单个agent可以完成单个区块的mining,worker的多个agent间应该是竞争关系。

// Agent can register themself with the worker
type Agent interface {
    Work() chan<- *Work
    SetReturnCh(chan<- *Result)
    Stop()
    Start()
    GetHashRate() int64
}

Agent接口的定义下面又有一个work结构,work主要用来表示挖掘一个区块时候所需要的数据环境。

// Work is the workers current environment and holds
// all of the current state information
type Work struct {
    // 链的配置属性
    config *params.ChainConfig
    signer types.Signer

    // 数据库状态
    state     *state.StateDB // apply state changes here
    // 祖先集,用来验证叔父块有效性
    ancestors *set.Set       // ancestor set (used for checking uncle parent validity)
    // 家庭集,用来验证叔块无效
    family    *set.Set       // family set (used for checking uncle invalidity)
    // 叔块集
    uncles    *set.Set       // uncle set
    // 交易量
    tcount    int            // tx count in cycle
    // 用于打包交易的可用天然气
    gasPool   *core.GasPool  // available gas used to pack transactions
    
    // 新区快
    Block *types.Block // the new block

    // 区块头
    header   *types.Header
    txs      []*types.Transaction
    receipts []*types.Receipt

    createdAt time.Time
}

这里有两个类实现了agent接口,分别是CpuAgent和RemoteAgent。CpuAgent是用cpu进行挖矿操作,RemoteAgent是远程挖矿,它提供了一套RPC接口来实现远程矿工进行采矿的功能。

type CpuAgent struct {
    // 同步锁
    mu sync.Mutex

    // work通道
    workCh        chan *Work
    // 结构体通道对象
    stop          chan struct{}
    quitCurrentOp chan struct{}
    // Result指针通道
    returnCh      chan<- *Result

    // 共识引擎
    chain  consensus.ChainReader
    engine consensus.Engine

    // 当前agent是否在挖矿
    isMining int32 // isMining indicates whether the agent is currently mining
}
...
type RemoteAgent struct {
    mu sync.Mutex

    quitCh   chan struct{}
    workCh   chan *Work
    returnCh chan<- *Result

    chain       consensus.ChainReader
    engine      consensus.Engine
    currentWork *Work
    work        map[common.Hash]*Work

    hashrateMu sync.RWMutex
    hashrate   map[common.Hash]hashrate

    running int32 // running indicates whether the agent is active. Call atomically
}

挖矿逻辑

开始挖矿首先要实例化一个miner对象。

// 创建miner对象
func New(eth Backend, config *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine) *Miner {
    miner := &Miner{
        eth:      eth,
        mux:      mux,
        engine:   engine,
        // 创建一个worker
        worker:   newWorker(config, engine, common.Address{}, eth, mux),
        canStart: 1,
    }
    // 注册newCpuAgent对象
    miner.Register(NewCpuAgent(eth.BlockChain(), engine))
    go miner.update()

    return miner
}

接着为实例化的miner对象创建一个worker对象来真正地干活。

// 为miner创建worker
func newWorker(config *params.ChainConfig, engine consensus.Engine, coinbase common.Address, eth Backend, mux *event.TypeMux) *worker {
    worker := &worker{
        config:         config,
        engine:         engine,
        eth:            eth,
        mux:            mux,
        // NewTxsEvent面熟吧,前面讲交易时 TxPool会发出该事件,当一笔交易被放入到交易池
        // 这时候如果work空闲会把Tx放到work.txs准备下一次打包进块
        txsCh:          make(chan core.NewTxsEvent, txChanSize),
        // ChainHeadEvent事件,表示已经有一个块作为链头 work.ipdate监听到该事件会继续挖矿
        chainHeadCh:    make(chan core.ChainHeadEvent, chainHeadChanSize),
        // ChainSideEvent事件,表示一个新块作为链的旁支可能会被放入possibleUncles中
        chainSideCh:    make(chan core.ChainSideEvent, chainSideChanSize),
        // 区块链数据库
        chainDb:        eth.ChainDb(),

        recv:           make(chan *Result, resultQueueSize),
        chain:          eth.BlockChain(),
        proc:           eth.BlockChain().Validator(),
        // 可能的叔块
        possibleUncles: make(map[common.Hash]*types.Block),
        coinbase:       coinbase,
        agents:         make(map[Agent]struct{}),
        // 挖出的未被确认的区块
        unconfirmed:    newUnconfirmedBlocks(eth.BlockChain(), miningLogAtDepth),
    }
    // Subscribe NewTxsEvent for tx pool
    worker.txsSub = eth.TxPool().SubscribeNewTxsEvent(worker.txsCh)
    // Subscribe events for blockchain
    worker.chainHeadSub = eth.BlockChain().SubscribeChainHeadEvent(worker.chainHeadCh)
    worker.chainSideSub = eth.BlockChain().SubscribeChainSideEvent(worker.chainSideCh)
    go worker.update()

    go worker.wait()
    worker.commitNewWork()

    return worker
}

从上面看出,worker.update方法来处理上述几个event事件。我们上次分析交易模块时对交易的提交也是在这处理的。

那么新区块的写入是在哪操作的呢?我们来来看worker.wait便能看出点端倪。


func (self *worker) wait() {
    for {
        for result := range self.recv {
            atomic.AddInt32(&self.atWork, -1)

            if result == nil {
                continue
            }
            block := result.Block
            work := result.Work

            // Update the block hash in all logs since it is now available and not when the
            // receipt/log of individual transactions were created.
            // 更新所有日志的块哈希
            for _, r := range work.receipts {
                for _, l := range r.Logs {
                    l.BlockHash = block.Hash()
                }
            }
            for _, log := range work.state.Logs() {
                log.BlockHash = block.Hash()
            }
            stat, err := self.chain.WriteBlockWithState(block, work.receipts, work.state)
            if err != nil {
                log.Error("Failed writing block to chain", "err", err)
                continue
            }
            // Broadcast the block and announce chain insertion event
            // 广播新区块并宣布链插入事件
            self.mux.Post(core.NewMinedBlockEvent{Block: block})
            var (
                events []interface{}
                logs   = work.state.Logs()
            )
            events = append(events, core.ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})

            if stat == core.CanonStatTy {
                events = append(events, core.ChainHeadEvent{Block: block})
            }

            self.chain.PostChainEvents(events, logs)

            // Insert the block into the set of pending ones to wait for confirmations
            // 将数据插入待处理块中,等待确认
            self.unconfirmed.Insert(block.NumberU64(), block.Hash())
        }
    }
}

接着回到miner的创建,在创建完worker后,会为worker注册agent。

func NewCpuAgent(chain consensus.ChainReader, engine consensus.Engine) *CpuAgent {
    miner := &CpuAgent{
        chain:  chain,
        engine: engine,
        stop:   make(chan struct{}, 1),
        workCh: make(chan *Work, 1),
    }
    return miner
}
...
func (self *Miner) Register(agent Agent) {
    if self.Mining() {
        agent.Start()
    }
    self.worker.register(agent)
}
...
func (self *worker) register(agent Agent) {
    self.mu.Lock()
    defer self.mu.Unlock()
    self.agents[agent] = struct{}{}
    agent.SetReturnCh(self.recv)
}

接下来在miner.update方法中开始挖矿。

// update keeps track of the downloader events. Please be aware that this is a one shot type of update loop.
// It's entered once and as soon as `Done` or `Failed` has been broadcasted the events are unregistered and
// the loop is exited. This to prevent a major security vuln where external parties can DOS you with blocks
// and halt your mining operation for as long as the DOS continues.
// update会跟踪下载程序事件。 请注意,这是一次性更新循环。
// 一旦广播“完成”或“失败”,事件就会被取消注册并退出循环。
// 这可以防止主要的安全漏洞,外部各方可以使用块来阻止你
// 并且只要DOS继续就停止你的挖掘操作
func (self *Miner) update() {
    events := self.mux.Subscribe(downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{})
out:
    for ev := range events.Chan() {
        switch ev.Data.(type) {
        // 下载开始
        case downloader.StartEvent:
            atomic.StoreInt32(&self.canStart, 0)
            if self.Mining() {
                self.Stop()
                atomic.StoreInt32(&self.shouldStart, 1)
                log.Info("Mining aborted due to sync")
            }
            // 下载完成或失败
        case downloader.DoneEvent, downloader.FailedEvent:
            shouldStart := atomic.LoadInt32(&self.shouldStart) == 1

            atomic.StoreInt32(&self.canStart, 1)
            atomic.StoreInt32(&self.shouldStart, 0)
            if shouldStart {
                // 开始挖矿
                self.Start(self.coinbase)
            }
            // unsubscribe. we're only interested in this event once
            events.Unsubscribe()
            // stop immediately and ignore all further pending events
            break out
        }
    }
}
...
func (self *Miner) Start(coinbase common.Address) {
    atomic.StoreInt32(&self.shouldStart, 1)
    self.SetEtherbase(coinbase)

    if atomic.LoadInt32(&self.canStart) == 0 {
        log.Info("Network syncing, will start miner afterwards")
        return
    }
    atomic.StoreInt32(&self.mining, 1)

    log.Info("Starting mining operation")
    // 真正开始挖矿
    self.worker.start()
    self.worker.commitNewWork()
}

worker.start()函数表示,真正进行挖矿的是worker。继续深入到对应代码。

func (self *worker) start() {
    self.mu.Lock()
    defer self.mu.Unlock()

    atomic.StoreInt32(&self.mining, 1)

    // spin up agents
    // 遍历所有的agent,通知agent开始挖矿
    for agent := range self.agents {
        agent.Start()
    }
}

然后,还不是真正挖矿的代码。还得继续深入虎穴来探究agent内部的start。

func (self *CpuAgent) Start() {
    if !atomic.CompareAndSwapInt32(&self.isMining, 0, 1) {
        return // agent already started
    }
    go self.update()
}

func (self *CpuAgent) update() {
out:
    for {
        select {
        // 检测是否有工作信号进入
        case work := <-self.workCh:
            self.mu.Lock()
            if self.quitCurrentOp != nil {
                close(self.quitCurrentOp)
            }
            self.quitCurrentOp = make(chan struct{})
            go self.mine(work, self.quitCurrentOp)
            self.mu.Unlock()
            // 监测停止信号
        case <-self.stop:
            self.mu.Lock()
            if self.quitCurrentOp != nil {
                close(self.quitCurrentOp)
                self.quitCurrentOp = nil
            }
            self.mu.Unlock()
            break out
        }
    }
}

func (self *CpuAgent) mine(work *Work, stop <-chan struct{}) {

    // 通过共识引擎算法来挖矿
    if result, err := self.engine.Seal(self.chain, work.Block, stop); result != nil {
        log.Info("Successfully sealed new block", "number", result.Number(), "hash", result.Hash())
        self.returnCh <- &Result{work, result}
    } else {
        if err != nil {
            log.Warn("Block sealing failed", "err", err)
        }
        self.returnCh <- nil
    }
}

这里,agent.update和miner.update逻辑类似。好在这里终于看到了最终挖矿时通过agent封装的共识引擎来实现的。

其次,还有一个worker. commitNewWork()方法,它主要完成待挖掘区块的数据组装。

// 完成待挖掘区块的数据组装
func (self *worker) commitNewWork() {

    // 相关锁设置
    self.mu.Lock()
    defer self.mu.Unlock()
    self.uncleMu.Lock()
    defer self.uncleMu.Unlock()
    self.currentMu.Lock()
    defer self.currentMu.Unlock()

    tstart := time.Now()
    parent := self.chain.CurrentBlock()

    tstamp := tstart.Unix()
    if parent.Time().Cmp(new(big.Int).SetInt64(tstamp)) >= 0 {
        tstamp = parent.Time().Int64() + 1
    }
    // this will ensure we're not going off too far in the future
    // 确保新区块的产生不会花费太多时间
    if now := time.Now().Unix(); tstamp > now+1 {
        wait := time.Duration(tstamp-now) * time.Second
        log.Info("Mining too far in the future", "wait", common.PrettyDuration(wait))
        time.Sleep(wait)
    }

    // 组装区块头信息
    num := parent.Number()
    header := &types.Header{
        ParentHash: parent.Hash(),
        Number:     num.Add(num, common.Big1),
        GasLimit:   core.CalcGasLimit(parent),
        Extra:      self.extra,
        Time:       big.NewInt(tstamp),
    }
    // Only set the coinbase if we are mining (avoid spurious block rewards)
    // 如果正在挖掘,设置coinbase
    if atomic.LoadInt32(&self.mining) == 1 {
        header.Coinbase = self.coinbase
    }
    // 确保区块头信息准备好
    if err := self.engine.Prepare(self.chain, header); err != nil {
        log.Error("Failed to prepare header for mining", "err", err)
        return
    }
    // If we are care about TheDAO hard-fork check whether to override the extra-data or not
    // TheDAO硬分叉相关设置
    if daoBlock := self.config.DAOForkBlock; daoBlock != nil {
        // Check whether the block is among the fork extra-override range
        limit := new(big.Int).Add(daoBlock, params.DAOForkExtraRange)
        // 根据新区块Number判断是否受TheDAO硬分叉的影响
        if header.Number.Cmp(daoBlock) >= 0 && header.Number.Cmp(limit) < 0 {
            // Depending whether we support or oppose the fork, override differently
            if self.config.DAOForkSupport {
                // 支持TheDAO硬分叉
                header.Extra = common.CopyBytes(params.DAOForkBlockExtra)
            } else if bytes.Equal(header.Extra, params.DAOForkBlockExtra) {
                header.Extra = []byte{} // If miner opposes, don't let it use the reserved extra-data
            }
        }
    }
    // Could potentially happen if starting to mine in an odd state.
    err := self.makeCurrent(parent, header)
    if err != nil {
        log.Error("Failed to create mining context", "err", err)
        return
    }
    // Create the current work task and check any fork transitions needed
    // 创建当前所需的工作环境work
    work := self.current
    // 给work设置相关硬分叉设置
    if self.config.DAOForkSupport && self.config.DAOForkBlock != nil && self.config.DAOForkBlock.Cmp(header.Number) == 0 {
        misc.ApplyDAOHardFork(work.state)
    }
    // 准备新区块的交易列表
    pending, err := self.eth.TxPool().Pending()
    if err != nil {
        log.Error("Failed to fetch pending transactions", "err", err)
        return
    }
    // 交易按价格和nonce值排序
    txs := types.NewTransactionsByPriceAndNonce(self.current.signer, pending)
    // 提交交易
    work.commitTransactions(self.mux, txs, self.chain, self.coinbase)

    // compute uncles for the new block.
    // 为新的区块计算叔块
    var (
        uncles    []*types.Header
        badUncles []common.Hash
    )
    for hash, uncle := range self.possibleUncles {
        if len(uncles) == 2 {
            break
        }
        if err := self.commitUncle(work, uncle.Header()); err != nil {
            log.Trace("Bad uncle found and will be removed", "hash", hash)
            log.Trace(fmt.Sprint(uncle))

            badUncles = append(badUncles, hash)
        } else {
            log.Debug("Committing new uncle to block", "hash", hash)
            uncles = append(uncles, uncle.Header())
        }
    }
    for _, hash := range badUncles {
        delete(self.possibleUncles, hash)
    }
    // Create the new block to seal with the consensus engine
    // 使用共识引擎对新区块进行赋值,包括Header.Root, TxHash, ReceiptHash, UncleHash几个属性
    if work.Block, err = self.engine.Finalize(self.chain, header, work.state, work.txs, uncles, work.receipts); err != nil {
        log.Error("Failed to finalize block for sealing", "err", err)
        return
    }
    // We only care about logging if we're actually mining.
    if atomic.LoadInt32(&self.mining) == 1 {
        log.Info("Commit new mining work", "number", work.Block.Number(), "txs", work.tcount, "uncles", len(uncles), "elapsed", common.PrettyDuration(time.Since(tstart)))
        self.unconfirmed.Shift(work.Block.NumberU64() - 1)
    }
    // 加载工作环境
    self.push(work)
    self.updateSnapshot()
}

总结下思路,首先挖矿这里又三个角色:miner,worker和agent。miner是以太坊封装的对外的挖矿接口,worker是给miner干活的,agent是真正实现挖矿的东东。一个miner都会有一个worker属性,一个worker又有多个agent来通过共识引擎实现真正挖矿,这里还有个work来存储挖矿所需要的数据环境。

当要开始挖矿时,miner会创建一个worker,并为之注册对应的agent。然后worker将一个work对象发送给agent,所有的agent根据共识引擎来挖矿,当一个agent完成mine时会将一个授权的block加上对应的work组成一个result对象返回给worker。

挖矿逻辑的源码就看完了,明天来继续看看关于agent挖矿到底是怎么实现共识的(ethash算法)。

更多以太坊源码解析请移驾全球最大同性交友网,觉得有用记得给个小star哦😯😯😯

.
.
.
.

互联网颠覆世界,区块链颠覆互联网!

--------------------------------------------------20181012 01:09
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270