以太坊源碼分析--挖礦與共識

ethereum.jpeg

挖礦(mine)是指礦工節(jié)點互相競爭生成新區(qū)塊以寫入整個區(qū)塊鏈獲得獎勵的過程.
共識(consensus)是指區(qū)塊鏈各個節(jié)點對下一個區(qū)塊的內(nèi)容形成一致的過程
在以太坊中, miner包向外提供挖礦功能,consensus包對外提供共識引擎接口

挖礦

miner包主要由miner.go worker.go agent.go 三個文件組成

  • Miner 負(fù)責(zé)與外部交互和高層次的挖礦控制
  • worker 負(fù)責(zé)低層次的挖礦控制 管理下屬所有Agent
  • Agent 負(fù)責(zé)實際的挖礦計算工作

三者之間的頂層聯(lián)系如下圖所示

worker_miner_agent.png

下面先從這幾個數(shù)據(jù)結(jié)構(gòu)的定義和創(chuàng)建函數(shù)來了解下它們之間的聯(lián)系

Miner

Miner的定義如下

type Miner struct{
    mux *event.TypeMux 
    worker *worker
    coinbase common.Address
    eth  Backend
    engine consensus.Engine
    .... 
}

各字段作用如下, 其中標(biāo)有的字段表示與Miner包外部有聯(lián)系

  • mux 接收來自downloader模塊的StartEvent DoneEvent FailedEvent事件通知。在網(wǎng)絡(luò)中,不可能只有一個礦工節(jié)點,當(dāng)downloader開始從其他節(jié)點同步Block時,我們就沒有必要再繼續(xù)挖礦了.
  • eth 通過該接口可查詢后臺TxPool BlockChain ethdb的數(shù)據(jù).舉例來說,作為礦工,我們在生成一個新的Block時需要從TxPool中取出pending Tx(待打包成塊的交易),然后將它們中的一部分作為新的Block中的Transaction
  • engine 采用的共識引擎,目前以太坊公網(wǎng)采用的是ethash,測試網(wǎng)絡(luò)采用clique.
  • worker 對應(yīng)的worker,從這里看出Miner和worker是一一對應(yīng)的
  • coinbase 本礦工的賬戶地址,挖礦所得的收入將計入該賬戶
  • mining 標(biāo)識是否正在挖礦

miner.New()創(chuàng)建一個Miner,它主要完成Miner字段的初始化和以下功能

  • 使用miner.newWorker()創(chuàng)建一個worker
  • 使用miner.newCpuAgent()創(chuàng)建Agent 并用Register方法注冊給worker
  • 啟動miner.update() 線程.該線程等待mux上的來自 downloader模塊的事件通知用來控制挖礦開始或停止

worker

worker成員比較多,其中部分成員的意義如下

  • mux engine eth coinbase 這幾項都來自與miner, 其中mux相對于Miner里的稍微有點不同, Miner里的mux是用來接收downloader的事件,而worker里用mux來向外部發(fā)布已經(jīng)挖到新Block
  • txCh 從后臺eth接收新的Tx的Channel
  • chainHeadCh 從后臺eth接收新的Block的Channel
  • recv 從agents接收挖礦結(jié)果的Channel,注意,每個管理的agent都可能將挖出的Block發(fā)到該Channel,也就是說,這個收方向Channel是一對多的
  • agents 管理的所有Agent組成的集合

miner.newWorker() 創(chuàng)建一個worker,它除了完成各個成員字段的初始化,還做了以下工作

  • 向后臺eth注冊txCh chainHeadCh chainSideCh通道用來接收對應(yīng)數(shù)據(jù)
  • 啟動worker.update() 線程.該線程等待上面幾個外部Channel 并作出相應(yīng)處理
  • 啟動worker.wait()線程.該線程等待Agent挖出的新Block
  • 調(diào)用worker.commitNewWork() 嘗試啟動新的挖掘工作

Agent

Agent(定義在worker.go)是一個抽象interface ,只要實現(xiàn)了其以下接口就可以充當(dāng)worker的下屬agent

type Agent interface {
    Work()   chan <-*Work
    SetReturnCh (chan<-*Result)
    Stop()
    Start()
    GetHashRate() int64
}

在agent.go中定義了CpuAgent作為一種Agent的實現(xiàn),其主要成員定義如下

type CpuAgent struct {
      workCh      chan *Work
      stop        chan struct{}
      returnCh    chan<-*Result
      chain     consensus.ChainReader
      engine   consensus.Engine
}
  • workCh 接收來自worker下發(fā)的工作任務(wù)Work
  • returnChworker反饋工作任務(wù)的完成情況,實際上就是挖出的新Block
  • stop 使該CpuAgent停止工作的信號
  • chain 用于訪問本地節(jié)點BlockChain數(shù)據(jù)的接口
  • engine 計算所采用的共識引擎
    CpuAgent的創(chuàng)建函數(shù)中并沒有啟動新的線程, Agent的工作線程是由Agent.Start()接口啟動的
    CpuAgent實現(xiàn)中,啟動了CpuAgent.update()線程來監(jiān)聽workChstop信道
func (self *CpuAgent) Start(){
      if !atomic.CompareAndSwapInt32(&self.isMining, 0, 1){
            return 
      }
      go self.update()
}

而Agent真正的挖礦工作是在收到工作任務(wù)'Work'后調(diào)用CpuAgent.mine()完成的

以上就是Miner worker Agent三者之間的聯(lián)系,將它們畫成一張圖如下:

總結(jié)以下就是

  • Miner監(jiān)聽后臺的數(shù)據(jù)
  • 需要挖礦時,worker發(fā)送給各個Agent工作任務(wù)Work, Agent挖出后反饋給worker

讓我們順著一次實際的挖掘工作看看一個Block是如何被挖掘出來的以及挖掘出之后的過程
worker.commitNewWork()開始

commitNewWork.png

1.parent Block是權(quán)威鏈上最新的Block
2.將標(biāo)識礦工賬戶的Coinbase填入Header,這里生成的Header只是個半成品
3.對于ehtash來說,這里計算Block的Difficulty
4.工作任務(wù)Work 準(zhǔn)確地說標(biāo)識一次挖掘工作的上下文Context,在創(chuàng)建時,它包含了當(dāng)前最新的各個賬戶信息state和2中生成的Header,在這個上下中可以通過調(diào)用work.commitTransactions()執(zhí)行這些交易,這就是俗稱的打包過程
5.礦工總是選擇Price高的交易優(yōu)先執(zhí)行,因為這能使其獲得更高的收益率,所以對于交易的發(fā)起者來說,如果期望自己的交易能盡快被所有人承認(rèn),他可以設(shè)置更高gasPrice以吸引礦工優(yōu)先打包這筆交易
6.運行EVM執(zhí)行這些交易
7.調(diào)用共識引擎的Finalize()接口
8.如此,一個Block的大部分原料都已經(jīng)準(zhǔn)備好了,下一步就是發(fā)送給Agent來將這個Block挖掘出來

當(dāng)Cpuagent收到Work后,調(diào)用mine()方法

func (self *CpuAgent) mine(work *Work, stop<-chan struct{}) {
        result, _  = self.engine.Seal(self.chain, work.Block, stop) 
        self.returnCh <- &Result{work,result}
}

可以看到實際上是調(diào)用的共識接口的Engine.Seal接口,挖掘的細(xì)節(jié)在后面共識部分詳述,這里先略過這部分且不考慮挖礦被Stop的情景,Block被挖掘出來之后將通過CpuAgent.returnCh反饋給workerworkerwait線程收到接口后將結(jié)果寫入數(shù)據(jù)庫,通過worker.mux向外發(fā)布NewMinedBlockEvent事件,這樣以太坊的其他在該mux上訂閱了該事件組件就可以收到這個事件

共識

共識部分包含由consensus對外提供共識引擎的接口定義,當(dāng)前以太坊有兩個實現(xiàn),分別是公網(wǎng)使用的基于POW的ethash包和測試網(wǎng)絡(luò)使用的基于POA的clique

根據(jù)前文的分析,在挖礦過程中主要涉及Prepare() Finalize() Seal() 接口,三者的職責(zé)分別為
Prepare() 初始化新Block的Header
Finalize() 在執(zhí)行完交易后,對Block進(jìn)行修改(比如向礦工發(fā)放挖礦所得)
Seal() 實際的挖礦工作

ethash

ethash是基于POW(Proof-of-Work),即工作量證明,礦工消耗算力來求得一個nonce,使其滿足難度要求HASH(Header) <= C / Diff,注意,這里的HASH是一個很復(fù)雜的函數(shù),而nonce是Header的一個成員字段,一旦改變nonce,左邊的結(jié)果將發(fā)生很大的變化。 C是一個非常大的常數(shù),Diff是Block的難度,可由此可知,Diff越大,右式越小,要想找到滿足不等式的nonce就越發(fā)的困難,而礦工正是消耗自己的算力去不斷嘗試nonce,如果找到就意味著他挖出這個區(qū)塊。
本文不打算詳述具體的HASH函數(shù),感興趣的讀者可以參考官方文檔https://github.com/ethereum/wiki/blob/master/Dagger-Hashimoto.md

Prepare()

ethash的Prepare()計算新Block需要達(dá)到的難度(Diffculty),這部分理論可見http://www.lxweimin.com/p/9e56faac2437

Finalize()

ethash的Finalize()向礦工節(jié)點發(fā)放獎勵,再Byzantium時期之前的區(qū)塊,挖出的區(qū)塊獎勵是5 ETH
,之后的獎勵3 ETH,這部分理論比較復(fù)雜,準(zhǔn)備以后專門寫一篇文章。

Seal()

下面來看看ethash具體是怎么實現(xiàn)Seal接口的

core/ethash/sealer.go
func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop<-chan struct{})(*types.Block, error){
   ......
   abort := make(chan struct{})
   found:= make(chan *types.Blocks)
   threads:= runtime.NumCPU()
   for i := 0; i < threads; i++ {
        go func(id int, nonce uint64){
             ethash.mine(block,id,nonce,abort,found)
        }(i, uint64(ethash.rand.Int63()))
   }
   var result *type.Block
   select{
       case <- stop:
       ....
       case result<-found:
       close(abort)
    }
    return result, nil
}

可以看到,ethash啟動了多個線程調(diào)用mine()函數(shù),當(dāng)有線程挖到Block時,會通過傳入的found通道傳出結(jié)果。

core/ethash/sealer.go
func (ethash *Ethash) mine(block *types.Block, id int, 
seed uint64, abort chan struct{}, found chan *types.Block) {
.....
search:
    for {
        select {
            case <-abort:   
            ......
            default:
            digest, result := hashimotoFull(dataset.dataset, hash, nonce)
            if new(big.Int).SetBytes(result).Cmp(target) <= 0 {
                // Correct nonce found, create a new header with it
                header = types.CopyHeader(header)
                header.Nonce = types.EncodeNonce(nonce)
                // Seal and return a block (if still needed)
                select {
                    case found <- block.WithSeal(header):
                    ......
                    case <-abort:
                }
                break search
            }
            nonce++
         }
    }
......

可以看到,在主要for循環(huán)中,不斷遞增nonce的值,調(diào)用hashimotoFull()函數(shù)計算上面公式中的左邊,而target則是公式的右邊。當(dāng)找到一個nonce使得左式<=右式時,挖礦結(jié)束,nonce填到header.Nonce

clique

以太網(wǎng)社區(qū)為開發(fā)者提供了基于POA(proof on Authortiy)的clique共識算法。與基于POS的ethash不同的是,clique挖礦不消耗礦工的算力。在clique中,節(jié)點分為兩類:

  • 經(jīng)過認(rèn)證(Authorized)的節(jié)點,在源碼里稱為signer,具有生成(簽發(fā))新區(qū)塊的能力,對應(yīng)網(wǎng)絡(luò)里的礦工
  • 未經(jīng)過認(rèn)證的節(jié)點,對應(yīng)網(wǎng)絡(luò)里的普通節(jié)點
    ethash中,礦工的賬戶地址存放在Header的Coinbase字段,但在clique中,這個字段另有他用。那么如何知道一個Block的挖掘者呢?答案是,礦工用自己的私鑰對Block進(jìn)行簽名(Signature),存放在Header的Extra字段,其他節(jié)點收到后,可以從這個字段提取出數(shù)字簽名以及簽發(fā)者(signer)的公鑰,使用這個公鑰可以計算出礦工(即signer)的賬戶地址。
    一個節(jié)點a的認(rèn)證狀態(tài)可以互相轉(zhuǎn)換,每個signer在簽發(fā)Block時,可以附帶一個提議(purposal),提議另一個本地記錄為非認(rèn)證的節(jié)點b轉(zhuǎn)變?yōu)檎J(rèn)證節(jié)點,或者相反。網(wǎng)絡(luò)中的其他節(jié)點c收到這個提議后,將其轉(zhuǎn)化為一張選票(Vote),如果支持節(jié)點的選票超過了節(jié)點c本地記錄的signer數(shù)量的一半,那么節(jié)點c就承認(rèn)節(jié)點b是signer

clique包由api.go clique.go snapshot.go三個文件組成
其中api.go中是一些提供給用戶的命令行操作,比如用戶可以輸入以下命令表示他支持b成為signer

clique.propose("賬戶b的地址", true)

clique.gosnapshot.go中分別定義兩個重要的數(shù)據(jù)結(jié)構(gòu)CliqueSnapshot
Clique數(shù)據(jù)結(jié)構(gòu)的主要成員定義如下

type  Clique struct {
    config *params.CliqueConfig
    recents      *lru.ARCCache
    signatures   *lrn.ARCCache
    proposals   map[common.Address]bool
    signer common.Address
    signFn  SignerFn
    ......
}
  • config 包含兩個配置參數(shù),其中Period設(shè)置模擬產(chǎn)生新Block的時間間隔,而Epoch表示每隔一定數(shù)量的Block就要把當(dāng)前的投票結(jié)果清空并存入數(shù)據(jù)庫,這么做是為了防止節(jié)點積壓過多的投票信息,類似于單機(jī)游戲中的存檔
  • recents 緩存最近訪問過的Snapshot,查詢的key為Block的Hash值,詳見之后的Snapshot
  • signatures 緩存最近訪問過的Block的signer,查詢的key為Block的Hash值
  • proposals 本節(jié)點待附帶的提議池,用戶通過propose()命名提交的提議會存放在這里,當(dāng)本節(jié)點作為礦工對一個Block進(jìn)行簽名時,會隨機(jī)選擇池中的一個提議附帶出去
  • signer 礦工節(jié)點的賬戶地址,意義上與ethash中的Coinbase類似
  • signFn 數(shù)字簽名函數(shù),它和signer都由Clique.Authorize()進(jìn)行設(shè)置,后者在eth/backend.go中的StartMining()中被調(diào)用

Snapshot翻譯過來是快照,它記錄了區(qū)塊鏈在特定的時刻(即特定的區(qū)塊高度)本地記錄的認(rèn)證地址列表,舉個栗子,Block#18731的Snapshot記錄了網(wǎng)絡(luò)中存在3個signer分別為a\b\c,且a已經(jīng)支持另一個節(jié)點d成為signer(a投了d一張支持票),當(dāng)Block#18732的挖掘者b也支持d時,Block#18732記錄的signer就會增加d的地址

type Snapshot struct{
    sigcache  *lru.ARCCache
    Number    uint64
    Hash    Common.Hash
    Signers map[Common.Address] struct{}
    Recents  map[uint64]common.Address
    Votes    []*Vote
    Tally    map[common.Address]Tally
}
  • sigcache 緩存最近訪問過的signer,key為Block的Hash值
  • Number 本Snapshot對應(yīng)的Block的高度,在創(chuàng)建時確定
  • Hash 本Snapshot對應(yīng)的Block的Hash,在創(chuàng)建時確定
  • Signers 本Snapshot對應(yīng)時刻網(wǎng)絡(luò)中認(rèn)證過的節(jié)點地址(礦工),在創(chuàng)建時確定
  • Recents 最近若干個Block的signer的集合,即挖出區(qū)塊的礦工
  • Votes 由收到的有效proposal計入的選票集合,每張選票記錄了投票人/被投票人/投票意見 這里的有效有兩層意思
    • 投票人是有效的的,首先他是signer(在Snapshot.Signers中),并且他不能頻繁投票(不在 Snapshot.Recents中)
    • 被投票人是有效的,被投票人的當(dāng)前認(rèn)證狀態(tài)與選票中攜帶的意見不同
  • Tally 投票結(jié)果map,key為被投票人地址,value為投票計數(shù)
Prepare()

Prepare()的實現(xiàn)分為兩部分

func (c *Clique) Prepare(chain consensus.ChainReader, header *types.Header){
    header.Coinbase = common.Address{}
    header.Nonce = types.BlockNonce{}
    number := header.Number.Uint64()

    snap, err := c.snapshot(chain, num-1, header.ParentHash, nil)
    if number % c.config.Epoch {
        addresses := make ([]common.Address)
        for address, authorize := range c.proposals{
            addresses = append(addresses, address)
        }
        header.Coinbase = addresses[rand.Intn(len(addresses))]
        if c.proposals[header.Coinbase] {
            copy(header.Nonce[:], nonceAuthVote)
        }  else {
            copy(header.Nonce[:], nonceDropVote)
        }
    }
    ......

首先獲取上一個Block的Snapshot,它有以下幾個獲取途徑

  • Clique的緩存
  • 如果Block的高度恰好是在checkpoint 就可從數(shù)據(jù)庫中讀取
  • 由一個之前已有的Snapshot經(jīng)過這之間的所有Header推算出來

接下來隨機(jī)地將本地proposal池中的一個目標(biāo)節(jié)點地址放到Coinbase (注意在ethash中,這個字段填寫的是礦工地址) 由于Clique不需要消耗算力,也就不需要計算nonce,因此在Clique中,Header的Nonce的字段被用來表示對目標(biāo)節(jié)點投票的意見

func (c *Clique) Prepare(chain consensus.ChainReader, header *types.Header){
   ......
   header.Difficulty = CalcDifficulty(snap, c.signer)
   header.Extra  = append(header.Extra, make([]byte, extraSeal))
   ......

接下來填充Header中的Difficulty字段,在Clique中這個字段只有 12 兩個取值,取決與本節(jié)點是否inturn,這完全是測試網(wǎng)絡(luò)為了減少Block區(qū)塊生成沖突的一個技巧,因為測試網(wǎng)絡(luò)不存在真正的計算,那么如何確定下一個Block由誰確定呢?既然都一樣,那就輪流坐莊,inturn的意思就是自己的回合,我們知道,區(qū)塊鏈在生成中很容易出現(xiàn)短暫的分叉(fork),其中難度最大的鏈為權(quán)威(canonocal)鏈,因此如果一個節(jié)點inturn,它就把難度設(shè)置為 2 ,否則設(shè)置為 1

前面提到過在Clique中,礦工的地址不是存放在Coinbase,而是將自己對區(qū)塊的數(shù)字簽名存放在Header的Extra字段,可以看到在Prepare()接口中為數(shù)字簽名預(yù)留了Extra的后 65 bytes

Finalize()

cliqueFinalize()操作比較簡單,就是計算了一下Header的Root Hash值

Seal()

Seal()接口相對ethash的實現(xiàn)來說比較簡單 (省略了一些檢查)

func (c *Clique) Seal (chain consensus.ChainReader, block *type.Block, stop <-chan struct{})  (*types.Block, error) {
    header := block.Header()
    signer, signFn := c.signer, c.signFn
    snap, err := c.snapshot(chain, number-1, header.ParentHash, nil)
    delay := time.Unix(header.Time.Int64(), 0).Sub(time.Now())
    ......
    select {
    case <- stop:
        return nil, nil
    case <-time.After(delay):
    }
    
    sighash, err := signFn(accounts.Account{Address:signer}, sigHash(header).Bytes())
    copy(header.Extra[len(header.Extra) - extraSeal:], sighash)
    return block.WithSeal(header), nil
}

總的來說就是延遲了一定時間后對Block進(jìn)行簽名,然后將自己的簽名存入header的Extra字段的后 65 bytes,為了減少沖突,對于不是inturn的節(jié)點還會多延時一會兒,上面的代碼我省略了這部分

總結(jié)

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

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

  • 以太坊的代碼中,名為miner的包負(fù)責(zé)挖礦的流程。其UML關(guān)系圖如下圖所示: 整體來說,就是一個礦工miner,擁...
    hukun閱讀 2,421評論 0 1
  • 我們都知道現(xiàn)在是一個信息大爆炸的社會,在各個領(lǐng)域充斥著各種干貨與雞湯,有很多網(wǎng)紅成為了超級IP,他們將知識體系化,...
    夢凝雪天閱讀 299評論 1 8
  • Q41 領(lǐng)導(dǎo)的NG行為有哪些? 1)不主動say hi 2)不和下屬聊工作之外的話題 3)中途打斷說話 4)貶低,...
    商未央閱讀 209評論 0 1