摘要
bitcoin是一种P2P形式的数字货币,本文主要从源码实现的角度上对bitcoin的内部架构、核心数据结构、核心功能模块流程(接收交易进内存池、打包处理区块、激活最长链等机制)、部分业务术语理解的分析总结.
程序架构
Bitcoin整体主要分为HTTP RPC SERVER、P2P、Mining(挖矿、区块打包)、交易内存池、UTXO、区块链验证(CChainState)、脚本执行、加解密等模块,按层次不同划分如下:
程序生命周期
Bitcoin按不同入口提供了bitcoind(后台进程)、bitcoin-qt(前台UI进程)、bitcoin-cli(终端程序),本文主要基于bitcoind进行分析。在正常情况下,Bitcoin从启动到结束主要历经3个阶段:基础设施初始化、区块链加载、节点激活运行,如下图所示:
其中参数初始化、HTTP server、加载区块链、激活节点对理解bitcoin运行过程比较重要,以下分别展开进行阐述.
参数对象的创建与初始化
Bitcoin按网络类型分为主网mainnet、测试网testnet、注册测试网regtestnet,要让节点运行在哪一个网络取决于配置文件中是否配置了“-regtest”、“-testnet”选项,默认为mainnet。不同的网络类型配置的参数也不一样:
1. std::unique_ptr CreateChainParams(const std::string& chain)
2. {
3. if (chain == CBaseChainParams::MAIN)
4. return std::unique_ptr(new CMainParams());
5. else if (chain == CBaseChainParams::TESTNET)
6. return std::unique_ptr(new CTestNetParams());
7. else if (chain == CBaseChainParams::REGTEST)
8. return std::unique_ptr(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(1,0);
43. base58Prefixes[SCRIPT_ADDRESS] = std::vector(1,5);
44. base58Prefixes[SECRET_KEY] = std::vector(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(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(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添加到候选区块集合以供后续激活最长链时使用。
接着是执行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 节点发现过程如下:
在成功获取其他节点地址后,通过Connman创建的另一个线程ThreadOpenAddedConnections来主动发起到其他节点的连接:
节点连接成功后通过唤醒ThreadMessageHandler调用PeerLogicValidation对象的InitializeNode函数对对方节点发送VERSION消息 开启两个节点间一系列后续交互。 Bitcoin对所有节点的socket事件(接受连接、接收数据、发送数据、连接关闭)统一在ThreadSocketHandler线程进行监听:
其中PeerLogicValidation对象主要封装了消息分发功能:
1. bool PeerLogicValidation::ProcessMessages(CNode* pfrom, std::atomic& 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& 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
Cache(UTXO)
CCoinsView主要提供了UTXO集合接口,底层数据增删改查由CCoinsViewDB实现。
交易内存池
LevelDB封装
存储到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下产生的。
区块链
核心功能流程
处理新交易
该流程主要是节点接收到一个交易时执行的过程(一般是钱包软件或其他节点发起),如下图所示
其中AcceptToMemeoryPool过程如下:
祖先交易、父交易、子交易如下图所示:
如果交易合法且满足添加条件,则最后会被添加进mempool.mapTx容器、并记录交易的父亲、子孙集合(mempool.mapLink),同时更新与父亲交易、子孙交易的关系映射、统计数据(内存使用),此时由于还没被打包成区块被成功挖矿,因此不会对UTXO产生影响(消耗输入、增加输出),在将交易成功添加进内存交易池后,会发送订阅消息给上层挖矿软件,当挖矿软件接收到通知后由挖矿软件决定是否将交易打包成区块区块,挖矿通过调用generateBlock RPC接口来打包区块执行工作量证明算法来挖矿,如果挖矿成功,则进入到processNewBlcok流程。
处理新区块
新区块到达后,首先对区块进行检查,如工作量证明是否合法、merkroot是否正确、区块大小是否符合限制、第一笔交易是否为coinbase交易等等,然后是尝试接受区块,将区块添加到mapBlockIndex、setBlockIndexCandidates集合,最后从setBlockIndexCandidates选择新Tip顶点、并激活最长链,如下图所示:
其中AcceptBlockHeader流程如下:
激活最长链是从候选区块集合中选择累计工作量证明最大的区块作为新链Tip,并找出新Tip所属的分支,如果与激活前的分支不是同一个分支,则进行分支切换(从fork点将旧分支卸下、链上新Tip所在的分支):
术语理解
时间戳
合法的时间戳必须大于前11个区块的中位数,并且小于网络调整时间+2小时。网络调整时间是节点能连接的所有节点的中位数,当然,这个时间不一定是一个严格准确的时间,甚至不能保证顺序。
nSequence相对时间锁
nSequence是“输入”结构体的字段,用于表示交易是否为“确定”,未确定的交易可以在内存池中修改,根据nSequence值大小范围来区别交易类型,如下所示:
对于具有nLocktime或CHECKLOCKTIMEVERIFY的交易,nSequence值必须设置为小于232,以使时间锁定器有效。通常设置为2^32 - 1(0xFFFFFFFE)。
见证隔离SegWit
“见证”指的是解锁脚本或者脚本签名,“见证隔离”指的是把脚本签名信息从交易中拿出来,放到一个新的数据结构“witness”中