2019.01.15

BTC.comマイニングプールのソースの分析

Pocket


こんにちは。L.Wです。 今回はBTC.comマイニングプールのソースを分析してみたので、皆さんと共有します。




BTC.comとは、大手マイニング企業であるBITMAINが運営するビットコインの代表的なマイニングプールです。 影響力を持つ企業が運営するマイニングプールなので、規模や安心感などは優れると思われています。




そもそもマイニングプールとは個人が単独でPoWマイニングを行うのではなく、複数のマイナーで協力してPoWマイニングを行う仕組みです。 これによって、個々のハッシュパワーが少なくともそれを統合する事で報酬が得やすくなりますので、プールでのマイニングは主流となります。




BTC.comマイニングプール はビットコイン(BTC)とビットコインキャッシュ(BCH)のマイニングではいずれもシェアトップ3の巨大なプールです。 もちろん、 これらの 主軸コインだけでなく、ETH、LTCなど7つ種類のコインが掘られています。




BTC.comマイニングプール の特徴として、以下のは挙げられます。







今回はこのプールのオープンソースを元に、インターネットでの関係資料も参照しながら、アーキテクチャと各モジュールの要点を纏めてみました。




アーキテクチャ




機能によって、モジュール化され、各モジュールはkafkaに通して通信します。マイニングマシン、即ちマイナーはStratumプロトコルでStratumサーバーモジュールと繋がっています。







以下は、モジュール毎に、機能とフローチャートを説明してみます。




GbtMaker モジュール




主な機能の取り纏め




  • Bitcoind ZMQのBITCOIND_ZMQ_HASHBLOCKというメッセージをリッスンし,新規ブロックが見つかると,直ぐにkafkaにGbt を送る。
  • デフォルト10秒間隔で(コンフィギュレーションファイルで変更可能)RPCでBitcoindノードに getblocktemplate をリクエストし,Gbtをkafkaに送る。
  • Gbtメッセージにはトランザクションリストを含めている。



getblocktemplateの例(詳しくは、このリンクに参照)




{
  "capabilities": [
    "proposal"
  ],
  "version": 536870912,
  "rules": [
    "csv",
    "segwit"
  ],
  "vbavailable": {
  },
  "vbrequired": 0,
  "previousblockhash": "0000000000000000002981dd215ec5d899e87f6c24595754690b652b9e72f810",
  "transactions": [
    {
      "data": "010000000177e788695235b5713b584d63222743480df850cb3976b1d5059b0d2daabbe06b000000006b483045022100cb1b237b5739f0fb4f99125d852a2671b60aca6f786e4c0d9b66b2ddbaad0da602203a5bdf274d975e3a19358ed5e1d1c49da19f0fc094ab2db554882e9e5d00b46301210389390a0c9e09703d66851f44c113d0c7f2c320d5ed324f9820d5463fde109d48ffffffff02e5c60300000000001976a91402302357eadae998a5085e85789818e9c609c03288ac0bb80a00000000001976a914b06a5ca4d62c158d2517c41160e5d1eb61382ca688ac00000000",
      "txid": "7a3416fcae579cab55f8c5dd063f6b27b0a5197af9287491b2f0ee035d13a761",
      "hash": "7a3416fcae579cab55f8c5dd063f6b27b0a5197af9287491b2f0ee035d13a761",
      "depends": [
      ],
      "fee": 50000,
      "sigops": 8,
      "weight": 904
    },
 ...
    {
      "data": "0100000001b075063f6e11f70a6a4285c0d5eb1f3377d98d1b3532d9b60491db60e6df502d000000006a47304402206cf3ada2502fc84ced09077cdd0c651c3802e2675543084fe5c8d0eea81660af02207e332a7fed2aa1b04957dc312b11b27e8aae50061ed1b6dfba899ad5086874f0012102aa231a16919e2b2532229813a41e497393e84d28a42276bf5b7d63cdacdd5005ffffffff020000000000000000536a4c500004dea30001f4c7decc62cf4795c96f71986dc48067a8f5271e2b157b649aac2590036ba991a14af220c9cad310a28fa9d9d86c5c32ad640626c2affa045340958f1a2f13df9014f19142c6a940acc3c6d10d00000000001976a914f67fc7a020eb8d83f081ffa4f1fb60de6d907f3588ac00000000",
      "txid": "8b68d5d6c8cef9ef8263b9f3d265aae1d05ba77d07453fd778677d9fc0500ee5",
      "hash": "8b68d5d6c8cef9ef8263b9f3d265aae1d05ba77d07453fd778677d9fc0500ee5",
      "depends": [
      ],
      "fee": 285,
      "sigops": 4,
      "weight": 1132
    }
  ],
  "coinbaseaux": {
    "flags": ""
  },
  "coinbasevalue": 1250361443,
  "longpollid": "0000000000000000002981dd215ec5d899e87f6c24595754690b652b9e72f810119724932",
  "target": "0000000000000000003218a50000000000000000000000000000000000000000",
  "mintime": 1546821831,
  "mutable": [
    "time",
    "transactions",
    "prevblock"
  ],
  "noncerange": "00000000ffffffff",
  "sigoplimit": 80000,
  "sizelimit": 4000000,
  "weightlimit": 4000000,
  "curtime": 1546825076,
  "bits": "173218a5",
  "height": 557383
}



フローチャート




※画像をクリックすると、拡大となる。


















I1210 05:57:33.390445 16035 GbtMaker.cc:171] gbt height: 553234, prev_hash: 0000000000000000001a79b3c06572e8f453c12dbf30880e3460f8cc7737e888, coinbase_value: 1271255924, bits: 1731d97c, mintime: 1544415072, version: 536870912|0x20000000,gbthash: b48411f8ae989b3dbde0978bcebc8dff67931833d955c28877f870708358a635
I1210 05:57:33.497092 16035 GbtMaker.cc:207] sumbit to Kafka, msg len: 3911528
I1210 05:57:38.643023 16035 GbtMaker.cc:171] gbt height: 553234, prev_hash: 0000000000000000001a79b3c06572e8f453c12dbf30880e3460f8cc7737e888, coinbase_value: 1271338454, bits: 1731d97c, mintime: 1544415072, version: 536870912|0x20000000,gbthash: 5c81caa425e7ff2b0ca5645374f2afbdeae16b6b9ee7c0ab12fe9f2ea009f2aa
I1210 05:57:38.731180 16069 GbtMaker.cc:242] >>>> bitcoind recv hashblock: 0000000000000000001c6be309ea239ebdc5f9ee0b3d8adfc18e176342401ee9, sequence: 1c860000 <<<<
I1210 05:57:38.731365 16069 GbtMaker.cc:251] get zmq message, call rpc getblocktemplate
I1210 05:57:38.758874 16035 GbtMaker.cc:207] sumbit to Kafka, msg len: 3909072
I1210 05:57:38.840273 16069 GbtMaker.cc:171] gbt height: 553235, prev_hash: 0000000000000000001c6be309ea239ebdc5f9ee0b3d8adfc18e176342401ee9, coinbase_value: 1251380229, bits: 1731d97c, mintime: 1544416048, version: 536870912|0x20000000,gbthash: deea77508fe1dbd3154483f7fc73073f86f6a77bd1035ce61d80c8b90350fa6a
I1210 05:57:38.867414 16069 GbtMaker.cc:207] sumbit to Kafka, msg len: 2923760



BlockMaker モジュール





主な機能の取り纏め




  • 複数のbitcoindノードと繋がることが可能。
  • 主に RAWGBT、STRATUM_JOB、SOLVED_SHAREとNMC_SOLVED_SHARE のこの四つ種類のメッセージをリッスンする。
  • RAWGBT をリッスンすることで、 gbtHash/トランザクションリストを取得でき、 vtxs ( トランザクションリスト )はブロックを作成することには欠かせない。 gbtHashとvtxsはrawGbtMap_ に保存される。(rawGbtMap_は最新のトップ100個のgbtHash/vtxs を格納する)。
  • STRATUM_JOB をリッスンすることで、 jobId_/gbtHash を取得できる。 jobId_とgbtHashはjobId2GbtHash_ に保存される。( jobId2GbtHash_は最新のトップ120個jobId_/gbtHashを格納する )。
  • SOLVED_SHARE をリッスンすることで、 BlockHeaderとcoinbaseTx を取得できる。( BlockHeader+coinbaseTx+vtxsでBlock を生成する)。
  • NMC_SOLVED_SHARE には、マージマイニングでNameCoinの採掘に使われる。
  • ローカルで作成されたブロックはbitcoinネットでの繋がっている他のノードに提出(submit)される。
  • 作成されたブロック情報とワーカー情報をデータベースに書き込む。



フローチャート





※画像をクリックすると、拡大となる。







DBでのmining_workers テーブル








DBでの found_blocks テーブル











PoolWatcherモジュール




主な機能の取り纏め




  • StratumJobメッセージをリッスンし、poolStratumJob_容器を更新し、他のプールと比較する。
  • クライアントとして他のプールと繋がり、マイニングのジョブを受け取る時には、受け取ったジョブからブロックの高さを抽出し、ローカルプールでのブロックの高さより1を大きい場合、空のEmptyGBTを構築し、kafkaに送る。
  • 以下のケースで受け取ったジョブを廃棄する。
    • Receivedジョブでのブロックの高さ = ローカルの高さ
    • Receivedジョブでのブロックの高さ <> ローカルの高さ +1 且つ高さの差が大きい(Jump too much)
    • ReceivedジョブでのブロックのnBits <> ローカルの nBits



フローチャート





※画像をクリックすると、拡大となる。







PoolWatcherのログの例




I1210 06:47:56.348021  6398 Watcher.cc:345] empty gbt: {"result":{"previousblockhash":"0000000000000000000d67010c4f3a809bbab5cb456922d1c1bae7b9892ac07a","height":553238,"coinbasevalue":1250000000,"bits":"1731d97c","mintime":1544423876,"curtime":1544424476,"version":536870912,"transactions":[]}}
I1210 06:47:56.374186  6398 Watcher.cc:518] &lt;SlashPool 001> prev block changed, height: 553238, prev_hash: 0000000000000000000d67010c4f3a809bbab5cb456922d1c1bae7b9892ac07a, block_time: 1544424476, nBits: 389142908, nVersion: 536870912
I1210 06:47:56.374363  6398 Watcher.cc:344] sumbit to Kafka, msg len: 452
I1210 06:47:56.374414  6398 Watcher.cc:345] empty gbt: {"result":{"previousblockhash":"0000000000000000000d67010c4f3a809bbab5cb456922d1c1bae7b9892ac07a","height":553238,"coinbasevalue":1250000000,"bits":"1731d97c","mintime":1544423876,"curtime":1544424476,"version":536870912,"transactions":[]}}
I1210 06:47:56.569010  6430 Watcher.cc:231] [POOL] prev block changed, height: 553238, prevhash: 0000000000000000000d67010c4f3a809bbab5cb456922d1c1bae7b9892ac07a, nBits: 389142908
I1210 06:47:56.848953  6398 Watcher.cc:518] &lt;antpool 003> prev block changed, height: 553238, prev_hash: 0000000000000000000d67010c4f3a809bbab5cb456922d1c1bae7b9892ac07a, block_time: 1544424476, nBits: 389142908, nVersion: 536870912
I1210 06:47:56.849122  6398 Watcher.cc:543] &lt;antpool 003> discard the job: height is same as pool. pool height: 553238, the job height: 553238






JobMakerモジュール




主な機能の取り纏め




  • kafkaの KAFKA_TOPIC_RAWGBTとKAFKA_TOPIC_NMC_AUXBLOCKをリッスンし、マージマイニング(merge mining)をサポートする。
  • Gbtメッセージを受け取る。ローカルの時間と60s以上の遅延の場合は、廃棄する。
  • gbtTime+isEmptyBlock+height をキーにして、ローカルMapに格納し、gbtHashもローカル配列に入れる。
  • ローカルの gbtHash の配列は20個しか保存しない。 ローカルgbtMapでのGbtの有効期間ですが、空ではないGbtの場合、90sとなり、空Gbtの場合、15sとなり、期限過ぎると、Mapから取り除かれる。(有効期間はコンフィグレーションファイルで定義可能)
  • 以下のケースで、直ちにkafkaに StratumJob を送る
    • 受け取ったGbtの高さ > ローカルの高さ (新規にブロックが見付かった)
    • 受け取ったGbtの高さ = ローカルの高さ  且つ ローカルの直前のブロックは空となる 且つ  受け取ったGbt は空ではない
    • 定義したインターバルを過ぎた(インターバルをコンフィグレーションファイルで定義可能)



フローチャート





※画像をクリックすると、拡大となる。
















kafkaに送られたStratumJobの例




sjob: {
  "jobId": 6633194262114099172,
  "gbtHash": "87785fe47714c9772c3b5ad082caab466c60f011c97366a63a7a6c5f1135bb5c",
  "prevHash": "000000000000000000094a077c7f7c0f6d24ec40af077f0814d8b7bb1851fcde",
  "prevHashBeStr": "1851fcde14d8b7bbaf077f086d24ec407c7f7c0f00094a070000000000000000",
  "height": 553222,
  "coinbase1": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff20030671082f474d4f2d5a2e636f6d2f",
  "coinbase2": "0409d70d5cffffffff02a601df4a000000001976a9143fdb793f78c514266d9cadb3c5b17f914993d31d88ac0000000000000000266a24aa21a9ed74fd960e7b764b5bbe822db04bb5fd3f8aed6579f5c8bbbf8f58eee5e81cc48a00000000",
  "merkleBranch": "9bbb7b427115820ac80982d006e 20ed9d12735c14f38e00d200a1178eabadd4f11783254f2ae046c3551b8b0f7f3799b0810818a92b200cd29c0862ab66fb3b6a33a3b9e0e034133fa515962c20ef004c9c332f1019289777f8f5603bad4c0f30e70bd4b6b409aee89ad55f6f4c4ef9e12a9a2f9efc1132800e8f23fdb795a78cb3b05d7 638372abb783b62f13a392c49595016d76109280de2a4e07a37d98c426dadb5bb154db378ecddba7fe72b1dba6e66c7be035fc98f5f10166e7b6dc21670d6f96e1e04e5b9a28484b9551a9b94d83bb8a14e130b673a2894b9791bd641a1e48734e48180ac12ef4ff43ddbab01dba64c0c80b73c6beef1 5f69eb1d9451ce8cfc31ee5aa8632ec2a3df8fe127aee0cabeb5be58f197e61daec73a8ce8594ea606081ee4cbb28478e7cf388aceef032df32b7ccb6e044a3b7b49beedd66ffa063bbc4d681202c4b425011f91a80a04ee99d6331fad92d48fd35063267a3",
  "nVersion": 536870912,
  "nBits": 389142908,
  "nTime": 1544410888,
  "minTime": 1544407755,
  "coinbaseValue": 1256128934,
  "witnessCommitment": "6a24aa21a9ed74fd960e7b764b5bbe822db04bb5fd3f8aed6579f5c8bbbf8f58eee5e81cc48a",
  "nmcBlockHash": "000000000000000000000000000000000000000000000000 0000000000000000",
  "nmcBits": 0,
  "nmcHeight": 32764,
  "nmcRpcAddr": "",
  "nmcRpcUserpass": "",
  "rskBlockHashForMergedMining": "",
  "rskNetworkTarget": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "rskFeesForMiner": "",
  "rskdRpcAdd ress": "",
  "rskdRpcUserPwd": "",
  "isRskCleanJob": false
}



jobmaker.cfgのコンフィグレーションファイルで報酬支払先のアドレスとプールのシンボルを定義する




pool = {
  //報酬の支払先
  payout_address = "my2dxGb5jz43ktwGxg2doUaEb9Wh8LIWEI";
  //coinbase info
  coinbase_info = "/GMO-Z.com//";
}



coinbaseトランザクションのフォーマット











StratumServerモジュール




主な機能の取り纏め




  • 受け取ったStratumJobには、遅延60s以上となると、廃棄される。
  • 受け取ったStratumJob でのpreviousHashはローカルのと同様ではない場合、新規にブロックが生成されたことと見做されて、jobの isClean ステータスをtrueにする。(trueの場合、マンナーに直ちに現在の計算作業を中止させ、最新のjobの計算作業にシフトさせる)
  • 三つケースではマンナーに新しいミッションを割り当てる。
    • 事前にセッティングしておいた job割り当てのインターバルを達した。(例えば、30s)
    • 直前のjobのブロックの高さは最新のと同じが、直前のは空ブロック、最新のは空ではない。
    • 最新のjobのブロックの高さは直前のより大きい。(つまり、最新のは一番大きい高さ数値)
  • 最新のミッションの割り当て の時刻はファイルに書き込まれる。(コンフィグレーションファイルでのfile_last_notify_timeの定義による)
  • ローカルでのjobの有効期間は300sとなる。
  • 10s毎にユーザーリスト( コンフィグレーションファイルでの
    list_id_api_url の定義による )を読み取り、Map容器に入れて保存する。
  • sserverの最大のSessionIdは16777214 となる。
  • Stratumプロトコル
    • suggest_targetはsuggest_difficultyと同じ機能を持ち,マイニングの難易度の調整に使われる。(subscribeの前にリクエストされる)
    • 各マイナーの計算作業を重複させないように、 sessionIDを extraNonce1_ とする。
    • 初期難易度は16384とセッティングされ,またはsuggest_difficulty方法で指定さて,workerのハッシュレート(シェア数)に応じて、Difficultyを調整 し、マイナーに10s毎にshareを提出させる。
    • session毎にlocalJobs_隊列を設けられて、隊列の長さは10にする。
    • マンナーをauthorizeしてから600sをカウントダウン開始し、600s以内にshareを受信できないと、 sessionのコネクションは切断される。
  • shareはRejectされるのケース
    • JOB_NOT_FOUND,localJobs_隊列ではすでにこのjobが見付からない。
    • JOB_NOT_FOUND,jobRepository_でのjobのステータスはStaleとなる,すなわちjobは古くなった。
    • JOB_NOT_FOUND, JobがjobRepository_に存在していない ,すなわちjobの有効期限を過ぎた(300s)
    • DUPLICATE_SHARE,shareはすでに提出済,提出済のshareはsubmitShares_ に退避する。
    • LOW_DIFFICULTY, 提出されたshareの難易度はTargetに満たさない。
    • TIME_TOO_OLD , shareでのnTimeがStartumJobのminTimeより古い
    • TIME_TOO_NEW , StartumJobのminTimeでのnTimeが現在の時間より 600秒新しい。



フローチャート





※画像をクリックすると、拡大となる。










Stratumプロトコルの例




※Stratumの他の例には、このリンクに参照できる。




Miner -> SServer ( subscribe )




{"id": 1, "method": "mining.subscribe", "params": ["cpuminer/2.5.0"]}



SServer -> Miner ( result notify)




{
    "id":1,
    "result":
    [
        [
            ["mining.set_difficulty","sessionid0100000000000000"], 
            ["mining.notify","sessionid0100000000000000"]
        ],
        "70000000",  // ExtraNonce1の16進数
        4   // ExtraNonce2_size
    ],
    "error":null
}



Miner -> SServer ( authorize )




{
    "id": 2, 
    "method": "mining.authorize", 
    "params": ["walletaddressbH4mMF2GpoLSY2v4qVquASTpzR4", "x"]
}



SServer -> Miner ( result notify, 認証通過とする)




{"id":2,"result":true,"error":null}



SServer -> Miner ( set_difficulty )




{"id":null,"method":"mining.set_difficulty","params":[8]}

※1ユニットのビットコインのディフィカルティは0x1d00ffffとなる。256ビット転換すると、0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffとなる。
上の[8]のディフィカルティは(0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff)/8となる。



SServer -> Miner ( jobを割り当て )




{
    "method":"mining.notify",
    "params":[
        "bf",  // job ID
        "af8cecebf98...43100000000", //previous block hash
        "010000000100000....0000000", //coinbase1
        "0d2f6e6f646.....5b36ec88ac00000000", //coinbase2
        ["6fb6....80cb","d4464...49a5a290", ... ,"68d803...4ec79d161"],// merkle branches
        "20000000", // block version
        "1a3fffc0", // nBits encoded network difficulty
        "5a158226",  //nTime,Timestamp
        false   // clean jobs,trueの場合には直ちに新jobにシフトする
     ]
}



Miner -> SServer ( submit )




{
    "method": "mining.submit", 
    "params": [
        "walletaddressbH4mMF2GpoLSY2v4qVquASTpzR4", //worker name
        "bf", //job ID
        "12300000", //extranonce2
        "5a158557", //nTime
        "6ae65681"  //nonce
    ], 
    "id":4
}



SServer -> Miner ( solvedshareとして承認する )




{
    "id":4,
    "result":true,
    "error":null
}







sharelogger 、slparser 、statshttpdモジュール




sharelogger モジュールの主な機能の取り纏め




  • kafkaからShareLogメッセージを受け取って、2s毎にファイルに書き込む。(コンフィグレーションファイルでの data_dirで定義)
    • 毎日ファイルを生成し、ファイル名のフォーマット: sharelog-yyyy-mm-dd.bin
    • 直近三日のファイルハンドルを維持できる。



slparserモジュールの主な機能の取り纏め




  • sharelogger モジュールで生成したファイルをリッスンし、ファイルを読み取り、15s毎に( コンフィグレーションファイルでのflush_db_interval で定義 )データベースに書き込む。
  • pool、worker、user単位に個々に Accept1h、Accept1d、score1h、score1d、Reject1h、Reject1d を統計する。
  • Httpdサービスも起動し、 ServerStatusとWorkerStatus をインタフェースで提供する。



slparserモジュールで取り扱うDBテーブル








statshttpdモジュールの主な機能の取り纏め




  • ShareLogを受け取り、worker、user毎に acceptCount_、acceptShareSec_、rejectShareMin_ を統計し、 totalWorkerCount_とtotalUserCount_ も統計する。
  • 15s毎に( コンフィグレーションファイルでのflush_db_intervalで定義 )ワーカー情報を データベースに書き込む。
    • accept1m_、accept5m_、accept15m_、reject15m_、accept1h_、reject1h_ 、acceptCount_、lastShareIP_、lastShareTime_ を統計する
  • COMMON_EVENTSメッセージをリッスンし、workerNameとminerAgentを取得し、テーブルmining_workers に更新する。
  • Httpdサービスも起動し、 ServerStatusとWorkerStatus をインタフェースで提供する。






まとめ




ソースを目を通したが、分からないことがまだあります。ソースを改修し、最適化、効率化できるか、これから深掘りしていきたいです。マイニングプールに興味ある方がこれを入門材料として、一緒に研究しましょう。




次世代システム研究室では、アプリケーション開発や設計を行うリードエンジニアを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。