Bitcoin(源码)架构与核心流程分析

摘要

bitcoin是一种P2P形式的数字货币,本文主要从源码实现的角度上对bitcoin的内部架构、核心数据结构、核心功能模块流程(接收交易进内存池、打包处理区块、激活最长链等机制)、部分业务术语理解的分析总结.

程序架构

Bitcoin整体主要分为HTTP RPC SERVER、P2P、Mining(挖矿、区块打包)、交易内存池、UTXO、区块链验证(CChainState)、脚本执行、加解密等模块,按层次不同划分如下:


image.png

程序生命周期

Bitcoin按不同入口提供了bitcoind(后台进程)、bitcoin-qt(前台UI进程)、bitcoin-cli(终端程序),本文主要基于bitcoind进行分析。在正常情况下,Bitcoin从启动到结束主要历经3个阶段:基础设施初始化、区块链加载、节点激活运行,如下图所示:

66B64F74-ADDF-4A99-9A08-CDD0077E6D66.png

其中参数初始化、HTTP server、加载区块链、激活节点对理解bitcoin运行过程比较重要,以下分别展开进行阐述.

参数对象的创建与初始化

Bitcoin按网络类型分为主网mainnet、测试网testnet、注册测试网regtestnet,要让节点运行在哪一个网络取决于配置文件中是否配置了“-regtest”、“-testnet”选项,默认为mainnet。不同的网络类型配置的参数也不一样:

1. std::unique_ptr<CChainParams> CreateChainParams(const std::string& chain)  
2.  {  
3.      if (chain == CBaseChainParams::MAIN)  
4.          return std::unique_ptr<CChainParams>(new CMainParams());  
5.      else if (chain == CBaseChainParams::TESTNET)  
6.          return std::unique_ptr<CChainParams>(new CTestNetParams());  
7.      else if (chain == CBaseChainParams::REGTEST)  
8.          return std::unique_ptr<CChainParams>(new CRegTestParams());  
9.      throw std::runtime_error(strprintf("%s: Unknown chain %s.", __func__, chain));  
10. }  
11.   
12. void SelectParams(const std::string& network)  
13. {  
14.     SelectBaseParams(network);  
15.     globalChainParams = CreateChainParams(network);  
16. }  
1.  CMainParams() {  
2.          strNetworkID = "main";  
3.          consensus.nSubsidyHalvingInterval = 210000; //奖励减半时间  
4.          consensus.BIP16Exception = uint256S("0x00000000000002dc756eebf4f49723ed8d30cc28a5f108eb94b1ba88ac4f9c22");  
5.          consensus.BIP34Height = 227931;  
6.          consensus.BIP34Hash = uint256S("0x000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8");  
7.          consensus.BIP65Height = 388381; // 000000000000000004c2b624ed5d7756c508d90fd0da2c7c679febfa6c4735f0  
8.          consensus.BIP66Height = 363725; // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931  
9.          consensus.powLimit = uint256S("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff");  
10.         consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks 两周调整一次难度  
11.         consensus.nPowTargetSpacing = 10 * 60; //10分钟生成一个区块  
12.         consensus.fPowAllowMinDifficultyBlocks = false;  
13.         consensus.fPowNoRetargeting = false;  
14.         consensus.nRuleChangeActivationThreshold = 1916; // 95% of 2016     BIP9允许部署多个向后兼容的软分叉,通过旷工在一个目标周期内投票,如果达到激活阈值nRuleChangeActivationThreshold,就能成功的启用该升级  
15.         consensus.nMinerConfirmationWindow = 2016; // nPowTargetTimespan / nPowTargetSpacing  
16.         consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 28;  
17.         consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime = 1199145601; // January 1, 2008  
18.         consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = 1230767999; // December 31, 2008  
19.   
20.         // Deployment of BIP68, BIP112, and BIP113.  
21.         consensus.vDeployments[Consensus::DEPLOYMENT_CSV].bit = 0;  
22.         consensus.vDeployments[Consensus::DEPLOYMENT_CSV].nStartTime = 1462060800; // May 1st, 2016  
23.         consensus.vDeployments[Consensus::DEPLOYMENT_CSV].nTimeout = 1493596800; // May 1st, 2017  
24.   
25.         // Deployment of SegWit (BIP141, BIP143, and BIP147)  
26.         consensus.vDeployments[Consensus::DEPLOYMENT_SEGWIT].bit = 1;  
27.         consensus.vDeployments[Consensus::DEPLOYMENT_SEGWIT].nStartTime = 1479168000; // November 15th, 2016.  
28.         consensus.vDeployments[Consensus::DEPLOYMENT_SEGWIT].nTimeout = 1510704000; // November 15th, 2017.  
29.   
30.         genesis = CreateGenesisBlock(1231006505, 2083236893, 0x1d00ffff, 1, 50 * COIN);  
31.         consensus.hashGenesisBlock = genesis.GetHash();  
32.         assert(consensus.hashGenesisBlock == uint256S("0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"));  
33.         assert(genesis.hashMerkleRoot == uint256S("0x4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"));  
34.         vSeeds.emplace_back("seed.bitcoin.sipa.be"); // Pieter Wuille, only supports x1, x5, x9, and xd  
35.         vSeeds.emplace_back("dnsseed.bluematt.me"); // Matt Corallo, only supports x9  
36.         vSeeds.emplace_back("dnsseed.bitcoin.dashjr.org"); // Luke Dashjr  
37.         vSeeds.emplace_back("seed.bitcoinstats.com"); // Christian Decker, supports x1 - xf  
38.         vSeeds.emplace_back("seed.bitcoin.jonasschnelli.ch"); // Jonas Schnelli, only supports x1, x5, x9, and xd  
39.         vSeeds.emplace_back("seed.btc.petertodd.org"); // Peter Todd, only supports x1, x5, x9, and xd  
40.         vSeeds.emplace_back("seed.bitcoin.sprovoost.nl"); // Sjors Provoost  
41.   
42.         base58Prefixes[PUBKEY_ADDRESS] = std::vector<unsigned char>(1,0);  
43.         base58Prefixes[SCRIPT_ADDRESS] = std::vector<unsigned char>(1,5);  
44.         base58Prefixes[SECRET_KEY] =     std::vector<unsigned char>(1,128);  
45.         base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x88, 0xB2, 0x1E};  
46.         base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4};  
47.   
48.         bech32_hrp = "bc";  
49.   
50.         vFixedSeeds = std::vector<SeedSpec6>(pnSeed6_main, pnSeed6_main + ARRAYLEN(pnSeed6_main));  
51. 
52.     }  
53. };  

从CMainparams的构造函数中可以发现挖矿难度调整周期、区块生成间隔时间、创世区块在此创建,以及P2P网络的DNS seed种子服务器列表等关键参数初始化,具体可详见代码。(bitcoin 节点发现有两种方式:1.通过DNS seed server获取其他节点地址,2.通过参数指定信任节点地址)

HTTP SERVER与RPC

Bticoin对外提供JSON-RPC服务, bitcoin基于libevent实现了一个HTTP JSON-RPC server,并在上面绑定了RPC接口路由,用于接收与回复外部的RPC请求:
往HTTP server注册URL handler

1.  bool StartHTTPRPC()  
2.  {  
3.      LogPrint(BCLog::RPC, "Starting HTTP RPC server\n");  
4.      if (!InitRPCAuthentication())  
5.          return false;  
6.    
7.      RegisterHTTPHandler("/", true, HTTPReq_JSONRPC);  
8.  #ifdef ENABLE_WALLET  
9.      // ifdef can be removed once we switch to better endpoint support and API versioning  
10.     RegisterHTTPHandler("/wallet/", false, HTTPReq_JSONRPC);  //往http server注册URL handler
11. #endif  
12.     assert(EventBase());  
13.     httpRPCTimerInterface = MakeUnique<HTTPRPCTimerInterface>(EventBase());  
14.     RPCSetTimerInterface(httpRPCTimerInterface.get());  
15.     return true;  
16. }  

处理JSONRPC请求、调用tableRPC内部对应的接口函数:

1.  static bool HTTPReq_JSONRPC(HTTPRequest* req, const std::string &)  
2.  {  
3.     
4.  ...  
5.      try {  
6.          // Parse request  
7.          UniValue valRequest;  
8.          if (!valRequest.read(req->ReadBody()))  
9.              throw JSONRPCError(RPC_PARSE_ERROR, "Parse error");  
10.   
11.         // Set the URI  
12.         jreq.URI = req->GetURI();  
13.   
14.         std::string strReply;  
15.         // singleton request  
16.         if (valRequest.isObject()) {  
17.             jreq.parse(valRequest);  
18.   
19.             UniValue result = tableRPC.execute(jreq);  
20.   
21.             // Send reply  
22.             strReply = JSONRPCReply(result, NullUniValue, jreq.id);  
23.   
24.         // array of requests  
25.         } else if (valRequest.isArray())  
26.             strReply = JSONRPCExecBatch(jreq, valRequest.get_array());  
27.         else  
28.             throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");  
29.   
30.         req->WriteHeader("Content-Type", "application/json");  
31.         req->WriteReply(HTTP_OK, strReply);  
32.     } catch (const UniValue& objError) {  
33.         JSONErrorReply(req, objError, jreq.id);  
34.         return false;  
35.     } catch (const std::exception& e) {  
36.         JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);  
37.         return false;  
38.     }  
39.     return true;  
40. }  

其中tableRPC是bitcoin的接口函数路由执行对象

1.  /** 
2.   * Call Table 
3.   */  
4.  static const CRPCCommand vRPCCommands[] =  
5.  { //  category              name                      actor (function)         argNames  
6.    //  --------------------- ------------------------  -----------------------  ----------  
7.      /* Overall control/query calls */  
8.      { "control",            "help",                   &help,                   {"command"}  },  
9.      { "control",            "stop",                   &stop,                   {}  },  
10.     { "control",            "uptime",                 &uptime,                 {}  },  
11. };  
12.   
13. CRPCTable::CRPCTable()  
14. {  
15.     unsigned int vcidx;  
16.     for (vcidx = 0; vcidx < (sizeof(vRPCCommands) / sizeof(vRPCCommands[0])); vcidx++)  
17.     {  
18.         const CRPCCommand *pcmd;  
19.   
20.         pcmd = &vRPCCommands[vcidx];  
21.         mapCommands[pcmd->name] = pcmd;  
22.     }  
23. }
1.  UniValue CRPCTable::execute(const JSONRPCRequest &request) const  
2.  {  
3.      // Find method  
4.      const CRPCCommand *pcmd = tableRPC[request.strMethod];  
5.      if (!pcmd)  
6.          throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Method not found");  
7.    
8.      g_rpcSignals.PreCommand(*pcmd);  
9.    
10.     try  
11.     {  
12.         // Execute, convert arguments to array if necessary  
13.         if (request.params.isObject()) {  
14.             return pcmd->actor(transformNamedArguments(request, pcmd->argNames));  
15.         } else {  
16.             return pcmd->actor(request);  
17.         }  
18.     }  
19.     catch (const std::exception& e)  
20.     {  
21.         throw JSONRPCError(RPC_MISC_ERROR, e.what());  
22.     }  
23. }  

加载区块链

程序在基础设施初始阶段完成后进入加载区块链阶段,首先是从leveldb KV数据库中加载区块索引(LoadBlcokIndex()),并根据区块的status添加到候选区块集合以供后续激活最长链时使用。

BC8B72E5-C270-43C9-A8DD-31184CEEBF02.png

接着是执行ReplayBlocks过程修复上次未完成block持久化事务就退出程序导致的不一致性问题,然后执行LoadChainTip过程从候选区块集合选择最大工作量的区块索引来激活最长链(选定最大工作量证明的区块作为区块链的顶点Tip,然后将该区块所在的分支依次加载最终形成最长链)

1.  //如果对leveldb里面存储的tip加载成功、则删除候选区块集合  
2.  bool LoadChainTip(const CChainParams& chainparams)  
3.  {  
4.      AssertLockHeld(cs_main);  
5.    
6.      if (chainActive.Tip() && chainActive.Tip()->GetBlockHash() == pcoinsTip->GetBestBlock()) return true;  
7.    
8.      if (pcoinsTip->GetBestBlock().IsNull() && mapBlockIndex.size() == 1) {  
9.          // In case we just added the genesis block, connect it now, so  
10.         // that we always have a chainActive.Tip() when we return.  
11.         LogPrintf("%s: Connecting genesis block...\n", __func__);  
12.         CValidationState state;  
13.         if (!ActivateBestChain(state, chainparams)) {  
14.             LogPrintf("%s: failed to activate chain (%s)\n", __func__, FormatStateMessage(state));  
15.             return false;  
16.         }  
17.     }  
18.   
19.     // Load pointer to end of best chain  
20.     CBlockIndex* pindex = LookupBlockIndex(pcoinsTip->GetBestBlock());  
21.     if (!pindex) {  
22.         return false;  
23.     }  
24.     chainActive.SetTip(pindex);  
25.   
26.     //删除候选区块集合  
27.     g_chainstate.PruneBlockIndexCandidates();  
28.   
29.     LogPrintf("Loaded best chain: hashBestChain=%s height=%d date=%s progress=%f\n",  
30.         chainActive.Tip()->GetBlockHash().ToString(), chainActive.Height(),  
31.         FormatISO8601DateTime(chainActive.Tip()->GetBlockTime()),  
32.         GuessVerificationProgress(chainparams.TxData(), chainActive.Tip()));  
33.     return true;  
34. }  

激活最长链过程见下文阐述。最后是验证VerifyDB过程,从链Tip开始往回迭代,对主链上的每一个区块执行最多4级验证(0-从磁盘中读区块,1-验证区块有效性,2-从磁盘中读undo,3-检查卸载tip时是否存在不一致状态)

激活节点

Bitcoin的P2P网络节点间通信基于Upnp协议实现(从源码中无发现UDP打洞代码),因此挖矿的节点需要运行在支持Upnp的网络环境中。bitcoin内部用Connman对象对节点发现、连接、socket事件、消息分发进行管理,分别由Connman创建的ThreadDNSAddressSeed、ThreadOpenAddedConnections、ThreadSocketHandler、ThreadMessageHandler 4个线程实现。
节点在启动时通过内置的DNS seed server域名或在参数中指定“-connect 具体的信任IP地址端口”进行节点发现,,其中DNS 节点发现过程如下:


B1C95954-047F-4DCE-A54F-DA6B11C98C05.png

在成功获取其他节点地址后,通过Connman创建的另一个线程ThreadOpenAddedConnections来主动发起到其他节点的连接:


73D202FA-D13F-4CF4-9C01-0470CAA84FB3.png

节点连接成功后通过唤醒ThreadMessageHandler调用PeerLogicValidation对象的InitializeNode函数对对方节点发送VERSION消息 开启两个节点间一系列后续交互。 Bitcoin对所有节点的socket事件(接受连接、接收数据、发送数据、连接关闭)统一在ThreadSocketHandler线程进行监听:


FD71FDF6-C77F-47AA-8CF3-CA4835AEE24D.png

其中PeerLogicValidation对象主要封装了消息分发功能:


92C30F65-C2FD-48C7-AF1D-0DB3391747ED.png
1.  bool PeerLogicValidation::ProcessMessages(CNode* pfrom, std::atomic<bool>& interruptMsgProc)  
2.  {  
3.      const CChainParams& chainparams = Params();  
4.    
5.      bool fMoreWork = false;  
6.    
7.      if (!pfrom->vRecvGetData.empty())  
8.          ProcessGetData(pfrom, chainparams.GetConsensus(), connman, interruptMsgProc);  
9.    
10.     if (pfrom->fDisconnect)  
11.         return false;  
12. …  
13.         fRet = ProcessMessage(pfrom, strCommand, vRecv, msg.nTime, chainparams, connman, interruptMsgProc);  
14. …..
15.     return fMoreWork;  
16. }  
1.  bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStream& vRecv, int64_t nTimeReceived, const CChainParams& chainparams, CConnman* connman, const std::atomic<bool>& interruptMsgProc)  
2.  {  
3.      LogPrint(BCLog::NET, "received: %s (%u bytes) peer=%d\n", SanitizeString(strCommand), vRecv.size(), pfrom->GetId());  
4.      if (gArgs.IsArgSet("-dropmessagestest") && GetRand(gArgs.GetArg("-dropmessagestest", 0)) == 0)  
5.      {  
6.          LogPrintf("dropmessagestest DROPPING RECV MESSAGE\n");  
7.          return true;  
8.      }  
9.      if (!(pfrom->GetLocalServices() & NODE_BLOOM) &&  
10.               (strCommand == NetMsgType::FILTERLOAD ||  
11.                strCommand == NetMsgType::FILTERADD))  
12.     {  
13.           
14.     }  
15.   
16.     if (strCommand == NetMsgType::REJECT)  
17.     {      
18.     }  
19.   ….
20.     else if (strCommand == NetMsgType::FILTERCLEAR)  
21.     {  
22.     }  
23.     else if (strCommand == NetMsgType::FEEFILTER) {  
24.         }  
25.     }  
26.     else if (strCommand == NetMsgType::NOTFOUND) {  
27.         // We do not care about the NOTFOUND message, but logging an Unknown Command  
28.         // message would be undesirable as we transmit it ourselves.  
29.     }  
30.     else {  
31.         // Ignore unknown commands for extensibility  
32.         LogPrint(BCLog::NET, "Unknown command \"%s\" from peer=%d\n", SanitizeString(strCommand), pfrom->GetId());  
33.     }  
34.     return true;  
35. }  

核心数据结构

P2P

114F6EF2-FFDA-4408-8947-AC9BD82F6298.png

Cache(UTXO)

43246DA9-1C70-47C4-B99A-1D591572C83C.png

CCoinsView主要提供了UTXO集合接口,底层数据增删改查由CCoinsViewDB实现。

交易内存池

325D00EA-5F80-4E4B-8DD8-C45B39337D4A.png
AEE5E333-E113-4970-A3B0-20850C30EB39.png
D7F23B04-F8DD-451C-888C-A193522B6105.png

LevelDB封装

6BF7BBDB-ED61-40BA-BADB-FF8FAB798AFF.png

存储到leveldb的block key-value存储格式如下:

  • ‘b’ + 32 字节的 block hash -> 记录块索引,每个记录存储:
    • 块头(block header)
    • 高度(height)
    • 交易的数量
    • 这个块在多大程度上被验证
    • 块数据被存储在哪个文件中
    • undo data 被存储在哪个文件中
  • ‘f’ + 4 字节的文件编号 -> 记录文件信息。每个记录存储:
    • 存储在具有该编号的块文件中的块的数量
    • 具有该编号的块文件的大小($ DATADIR / blocks / blkNNNNN.dat)
    • 具有该编号的撤销文件的大小($ DATADIR / blocks / revNNNNN.dat)
    • 使用该编号存储在块文件中的块的最低和最高高度
    • 使用该编号存储在块文件中的块的最小和最大时间戳
  • ‘l’ – > 4个字节的文件号:使用的最后一个块文件号。
  • ‘R’ – > 1字节布尔值(如果为“1”):是否处于重新索引过程中。
  • ‘F’+ 1个字节的标志名长度+标志名字符串 – > 1个字节布尔型(’1’为真,’0’为假):可以打开或关闭的各种标志。 目前定义的标志是 ‘txindex’:是否启用事务索引。
    • ‘t’+ 32字节的交易 hash – >记录交易索引。 这些是可选的,只有当’txindex’被启用时才存在。 每个记录存储:
    • 哪个文件中的交易所属的块被抵消存储在
    • 从该块的开始到该交易本身被存储的位置的偏移量
    • 交易存储在哪个块文件号码中

存储到leveldb的undo-block key-value存储格式如下:

  • ‘c’+ 32字节的交易hash – >记录该交易未花费交易输出。 这些记录仅对至少有一个未使用输出的事务处理。 每个记录存储:
    • 交易的版本。
    • 交易是否是一个coinbase或没有。
    • 哪个高度块包含交易。
    • 该交易的哪些输出未使用。
    • scriptPubKey和那些未使用输出的数量。
  • ‘B’ – > 32字节block hash:记录UTXO是在那个block下产生的。

区块链

64623653-7384-4EF7-959F-C68F779C8DDD.png

核心功能流程

处理新交易

该流程主要是节点接收到一个交易时执行的过程(一般是钱包软件或其他节点发起),如下图所示


B9188830-2D8D-47D0-80B0-2B758DA2380E.png

其中AcceptToMemeoryPool过程如下:


3827169C-4F7A-4994-A56B-1B41B2972797.png

祖先交易、父交易、子交易如下图所示:


9042BB03-6B6E-4024-A090-1E7DF08CAE99.png

如果交易合法且满足添加条件,则最后会被添加进mempool.mapTx容器、并记录交易的父亲、子孙集合(mempool.mapLink),同时更新与父亲交易、子孙交易的关系映射、统计数据(内存使用),此时由于还没被打包成区块被成功挖矿,因此不会对UTXO产生影响(消耗输入、增加输出),在将交易成功添加进内存交易池后,会发送订阅消息给上层挖矿软件,当挖矿软件接收到通知后由挖矿软件决定是否将交易打包成区块区块,挖矿通过调用generateBlock RPC接口来打包区块执行工作量证明算法来挖矿,如果挖矿成功,则进入到processNewBlcok流程。

处理新区块

新区块到达后,首先对区块进行检查,如工作量证明是否合法、merkroot是否正确、区块大小是否符合限制、第一笔交易是否为coinbase交易等等,然后是尝试接受区块,将区块添加到mapBlockIndex、setBlockIndexCandidates集合,最后从setBlockIndexCandidates选择新Tip顶点、并激活最长链,如下图所示:


052D15A4-E824-44E2-9DBF-81711621CFFA.png

其中AcceptBlockHeader流程如下:


5F57D457-1A96-4037-B28D-0395759A0083.png
D7D7C268-6301-4C7B-AF88-523F26F3E1EC.png

激活最长链是从候选区块集合中选择累计工作量证明最大的区块作为新链Tip,并找出新Tip所属的分支,如果与激活前的分支不是同一个分支,则进行分支切换(从fork点将旧分支卸下、链上新Tip所在的分支):


4D3C2319-B3EA-46B7-BAA7-A8CE54291470.png

术语理解

时间戳

合法的时间戳必须大于前11个区块的中位数,并且小于网络调整时间+2小时。网络调整时间是节点能连接的所有节点的中位数,当然,这个时间不一定是一个严格准确的时间,甚至不能保证顺序。

nSequence相对时间锁

nSequence是“输入”结构体的字段,用于表示交易是否为“确定”,未确定的交易可以在内存池中修改,根据nSequence值大小范围来区别交易类型,如下所示:


538AAC9E-CB93-4466-B30C-380343B1AD4E.png

对于具有nLocktime或CHECKLOCKTIMEVERIFY的交易,nSequence值必须设置为小于232,以使时间锁定器有效。通常设置为2^32 - 1(0xFFFFFFFE)。

见证隔离SegWit

“见证”指的是解锁脚本或者脚本签名,“见证隔离”指的是把脚本签名信息从交易中拿出来,放到一个新的数据结构“witness”中

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

推荐阅读更多精彩内容