🐣

Bitcoinネットワークにおけるピアノードの発見

2022/02/13に公開

This document contains a copy of articles in bitcoin developer.

タイトルに挙げたトピックについて、Bitcoinの実際のプログラムであるBitcoin Coreでどのように実装されているかを理解する。

ピア・ツー・ピアネットワーク(Peer-to-peer network)

The Bitcoin network protocol allows full nodes (peers) to collaboratively maintain a peer-to-peer network for block and transaction exchange.

Bitcoinネットワークはいわゆるピア・ツー・ピアネットワークであり、ネットワーク上にある多数のフルノード(あるいはピア)と呼ばれるプログラム同士が、協調してブロックやトランザクション(取引)を送受信することで成り立っている。

ピアの発見(Peer Discovery)

When started for the first time, programs don’t know the IP addresses of any active full nodes. In order to discover some IP addresses, they query one or more DNS names (called DNS seeds) hardcoded into Bitcoin Core and BitcoinJ. The response to the lookup should include one or more DNS A records with the IP addresses of full nodes that may accept new incoming connections.

初めて起動する時、プログラムは通信相手となるフルノードのIPアドレスを知らない[1]。このため、プログラム内部にハードコードされたいくつかのドメイン名(これらはDNSシードと呼ばれる)について、dignslookupがするのと同じ様にDNSに問い合わせをすることにより、ピアのIPアドレスを発見する。ハードコードされたドメイン名についての、DNSへの問い合わせ(名前解決の要求)の応答は、新たな接続を受け入れるであろうフルノードのIPアドレス(DNS Aレコード)となる。1つのドメイン名に対して、1つ以上のIPアドレスが応答される。

The DNS seeds are maintained by Bitcoin community members: some of them provide dynamic DNS seed servers which automatically get IP addresses of active nodes by scanning the network; others provide static DNS seeds that are updated manually and are more likely to provide IP addresses for inactive nodes. In either case, nodes are added to the DNS seed if they run on the default Bitcoin ports of 8333 for mainnet or 18333 for testnet.

DNSシードはBitcoinコミュニティによって維持運用されている。いくつかは動的DNSシードであり、Bitcoinネットワークをスキャンすることで自動的に取得したピアのIPアドレスを提供する。静的DNSシードは、提供するIPアドレスを手動で更新しており、稼働していないピアのものである可能性が比較的高い。いずれのDNSシードが提供するIPアドレスにも、既定のポート(mainnetであれば8333、testnetであれば18333)上で稼働しているピアのものが登録される。

Bitcoin Coreでの実装

Bitcoin Core(bitcoind)を実行すると、main()から順に関数を呼び出した先で、生成されるスレッドThreadDNSAddressSeed内で、DNSシードからピアのIPアドレスを問い合わせる。

main()-> AppInit()

src/bitcoind.cpp
 250: int main(int argc, char* argv[])
 251: {
    ...
 257:     NodeContext node;
    ...
 269:     return (AppInit(node, argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE);
 270: }

 108: static bool AppInit(NodeContext& node, int argc, char* argv[])
 109: {
    ...
 146:     try
 147:     {
    ...
 155:         try {
 156:             SelectParams(args.GetChainName());
 157:         } catch (const std::exception& e) {
 158:             return InitError(Untranslated(
                                       strprintf("%s\n", e.what())));
 159:         }
    ...
 226:         fRet = AppInitInterfaces(node) && AppInitMain(node);
 227:     }

AppInitMain()

src/init.cpp
1063: bool AppInitMain(NodeContext& node,
                           interfaces::BlockAndHeaderTipInfo* tip_info)
1064: {
    ...
1780:     if (!node.connman->Start(*node.scheduler, connOptions)) {
1781:         return false;
1782:     }

CConnman::Start()

src/net.cpp
2510: bool CConnman::Start(CScheduler& scheduler, const Options& connOptions)
2511: {
    ...
2587:     if (!gArgs.GetBoolArg("-dnsseed", DEFAULT_DNSSEED))
2588:         LogPrintf("DNS seeding disabled\n");
2589:     else
2590:         threadDNSAddressSeed = std::thread(&util::TraceThread,
                             "dnsseed", [this] { ThreadDNSAddressSeed(); });

ThreadDNSAddressSeed()では、まずDNSシードの一覧を取得する(①)。そして、取得したそれぞれのDNSシードに(②)ピアに要求する機能を表すビットrequiredServiceBitsを付けたホスト名host[2]について(③)、LookupHost()でDNSへの問い合わせを実施し、結果(IPアドレスの一覧)をvIPsに取得する(④)。取得したIPアドレスと既定のポートの情報を合わせて、ピア(接続先のサービス)のアドレスaddrを作成し(⑤)、その一覧vAddをaddress manageraddrmanに追加する(⑥)。

src/net.cpp
1649: void CConnman::ThreadDNSAddressSeed()
1650: {
1651:     FastRandomContext rng;
1652:     std::vector<std::string> seeds = Params().DNSSeeds();  ///①
    ...
1681:     for (const std::string& seed : seeds) {                ///②
    ...
1726:         LogPrintf("Loading addresses from DNS seed %s\n", seed);
    ...
1730:             std::vector<CNetAddr> vIPs;
1731:             std::vector<CAddress> vAdd;
1732:             ServiceFlags requiredServiceBits
                         = GetDesirableServiceFlags(NODE_NONE);
1733:             std::string host = strprintf
                         ("x%x.%s", requiredServiceBits, seed);  ///③
    ...
1739:             if (LookupHost(host, vIPs, nMaxIPs, true)) {   ///④
1740:                 for (const CNetAddr& ip : vIPs) {
1741:                     int nOneDay = 24*3600;
1742:                     CAddress addr = CAddress(
                              CService(ip, Params().GetDefaultPort()), ///⑤
                              requiredServiceBits);
1743:                     addr.nTime = GetTime()
                              - 3*nOneDay - rng.randrange(4*nOneDay); 
                              // use a random age between 3 and 7 days old
1744:                     vAdd.push_back(addr);
1745:                     found++;
1746:                 }
1747:                 addrman.Add(vAdd, resolveSource);          ///⑥
1748:             } else {
    ...
1755:     }
1756:     LogPrintf("%d addresses found from DNS seeds\n", found);
1757: }

ThreadDNSAddressSeedで取得するDNSシードと既定のポートは、このスレッドが動作し始める前に、参加するBitcoinネットワークの種別に応じて、SelectParams()により設定する。例えば、mainnetであれば(①)、CMainParamsのコンストラクタにより、既定のポートnDefaultPort(②)とDNSシードvSeeds(③)を設定する。

src/chainparams.cpp
557: void SelectParams(const std::string& network)
558: {
559:     SelectBaseParams(network);
560:     globalChainParams = CreateChainParams(gArgs, network);
561: }
    ...
543: std::unique_ptr<const CChainParams> CreateChainParams
                         (const ArgsManager& args, const std::string& chain)
544: {
545:     if (chain == CBaseChainParams::MAIN) {                  ///①
546:         return std::unique_ptr<CChainParams>(new CMainParams());
547:     } else if (chain == CBaseChainParams::TESTNET) {
548:         return std::unique_ptr<CChainParams>(new CTestNetParams());
549:     } else if (chain == CBaseChainParams::SIGNET) {
550:         return std::unique_ptr<CChainParams>(new SigNetParams(args));
551:     } else if (chain == CBaseChainParams::REGTEST) {
552:         return std::unique_ptr<CChainParams>(new CRegTestParams(args));
553:     }
554:     throw std::runtime_error(
             strprintf("%s: Unknown chain %s.", __func__, chain));
555: }
    ...
 61: class CMainParams : public CChainParams {
 62: public:
 63:     CMainParams() {
 64:         strNetworkID = CBaseChainParams::MAIN;
    ... 
106:          nDefaultPort = 8333;                               ///②
    ...
112:         vSeeds.emplace_back("seed.bitcoin.sipa.be");        ///③
                         // Pieter Wuille, only supports x1, x5, x9, and xd
113:         vSeeds.emplace_back("dnsseed.bluematt.me");
                         // Matt Corallo, only supports x9
114:         vSeeds.emplace_back("dnsseed.bitcoin.dashjr.org"); 
                         // Luke Dashjr
    ...
174:     }
175: };

ThreadDNSAddressSeedでLookupHost()を呼び出したときに、実際にDNSへの問い合わせを実施する(①)のは、関数ポインタg_dns_lookup[3]に設定されたWrappedGetAddrInfoであり(②)、WrappedGetAddrInfo()は内部でシステムライブラリgetaddrinfo[4]をコールしてDNSへの問い合わせをし(③)、Bitcoin CoreでのIPアドレスの表現であるCNetAddrにして値を返している。

src/netbase.cpp
169: bool LookupHost(const std::string& name, std::vector<CNetAddr>& vIP,
                               unsigned int nMaxSolutions, bool fAllowLookup,
                               DNSLookupFn dns_lookup_function)
170: {
    ...
181:     return LookupIntern(strHost, vIP, nMaxSolutions, fAllowLookup,
                                dns_lookup_function);
182: }
    ...
134: static bool LookupIntern(const std::string& name,
                                std::vector<CNetAddr>& vIP,
                                unsigned int nMaxSolutions,
                                bool fAllowLookup,
                                DNSLookupFn dns_lookup_function)
135: {
136:     vIP.clear();
    ...
156:     for (const CNetAddr& resolved :
                      dns_lookup_function(name, fAllowLookup)) {   ///①
157:         if (nMaxSolutions > 0 && vIP.size() >= nMaxSolutions) {
158:             break;
159:         }
160:         /* Never allow resolving to an internal address.
                Consider any such result invalid */
161:         if (!resolved.IsInternal()) {
162:             vIP.push_back(resolved);
163:         }
164:     }
165: 
166:     return (vIP.size() > 0);
167: }
    ...
 87: DNSLookupFn g_dns_lookup{WrappedGetAddrInfo};                  ///②
    ...
 45: std::vector<CNetAddr> WrappedGetAddrInfo(const std::string& name,
                                             bool allow_lookup)
 46: {
 47:     addrinfo ai_hint{};
     ...
 61:     addrinfo* ai_res{nullptr};
 62:     const int n_err{getaddrinfo                                ///③
                           (name.c_str(), nullptr, &ai_hint, &ai_res)};
     ...
 68:     addrinfo* ai_trav{ai_res};
 69:     std::vector<CNetAddr> resolved_addresses;
     ...
 78:             resolved_addresses.emplace_back(
                                      reinterpret_cast<sockaddr_in*>
                                          (ai_trav->ai_addr)->sin_addr);
     ...
 84:     return resolved_addresses;
 85: }
脚注
  1. ユーザが、ピアのIPアドレスを知っていれば起動時のパラメータで指定することもできる。 ↩︎

  2. 例えば、機能9をサポートしたピアをDNSシードexsample.seed.bitcoin.orgから調べるには、x9.exsample.seed.bitcoin.orgに対応するIPアドレスを問い合わせる。 ↩︎

  3. LookupHost()の宣言により、第5引数dns_lookup_functionが省略された時の値として指定されている。 ↩︎

  4. Man Page of GETADDRINFO ↩︎

Discussion