初のAdvent Calendar参加です。1日遅れですが、暗号通貨Advent Calendar 5日目に登録しました。
お茶漬けと申します。普段は都内で学生やってます。研究進まない。
昨日4日目は@zinntikumugai氏の仮想通貨を変換した時の金額を求めるTwitterBOT作ったお話でした。JPYやBTCを挟まずに、目的の通貨へのレートを一発で教えてもらえるTwitterBOTだそうです。中間に基準通貨のようなものを挟まずともレートを教えてくれるというのは、便利で良さそうです。新たに対応通貨を増やす等、精力的に活動しているようで、今後が楽しみですね。
さて、プールマイニングを行った場合に、報酬はShareの比率に従って分配されるとかなんとか言いますが、その比率とは何を元に算出されるのか、きちんと考えたことが無かったので、具体的に理解する事が目標です。報酬の分配方式としては、代表的なものにPPLNSがあると思いますが、ここではPROPのみに絞ります(単純に私の技量と気力が足りなかったので)。また、実装を見させてもらうのは、MPOS/php-mposです。
基本的にはgithubのソースを眺めて追いかけていきますが、随時データベースの中身も利用します(採掘経験のある、mposデータベースが手元にあるので)。
ちなみに私は、きちんとPHPを触ったことがない上に、人のコードを読む経験が非常に少ないので、かなり手探りになります。ご了承ください。また、間違い等あったらご指摘頂けると大変嬉しく思います。
TL;DR
比率の計算は各ユーザの提出したShare達のDifficultyの総和を元に行う。おしまい。
以下はphp初心者が手探りで読んでいった痕跡です。
眺める起点
/cronjobs/run-crons.sh
をcronで回すので、それを見てみる
~略~ # List of cruns to execute CRONS="findblock.php proportional_payout.php pplns_payout.php pps_payout.php blockupdate.php payouts.php tickerupdate.php notifications.php statistics.php tables_cleanup.php" ~略~ for cron in $CRONS; do [[ $VERBOSE == 1 ]] && echo "Running $cron, check logfile for details" $PHP_BIN $cron $PHP_OPTS done
- $CRONSの中に
proportional_payout.php
とかいうドンピシャくさいものがある
proportional_payout.php
を読んでみる
- creditで検索してみると、122行目に
Add new credit transaction
という文言がある そこの周辺はこんな感じ
// Add new credit transaction if (!$transaction->addTransaction($aData['id'], $aData['payout'], 'Credit', $aBlock['id'])) $log->logFatal('Failed to insert new Credit transaction to database for ' . $aData['username'] . ': ' . $transaction->getCronError());
- 「
$transaction->addTransaction()
に失敗したらログに出力するよ」みたいに見える addTransaction()
の引数にはid
,payout
,Credit
とかそれっぽいキーワードが並ぶaddTransaction()
の定義はどこに?$transaction
の定義を探せば良い?$transaction
はこの行が初出なのでこのファイルには無さそうtransaction->addTransaction
でリポジトリ検索してみると、include/classes/transaction.class.php
が見つかる5行目に
$table = 'transactions'
があって、クエリにはINERT INTO $this->table...
とか書いてあるので、transactionsテーブルをいじるクラスだと想像される18行目、addTransactionの引数リストを見ると、account_id, amount, type, …と並んでいるので、上記の
$aData['payout']
は払い出しの分量っぽいことがわかるでは
$aData['payout']
どうやって決定される?proportional_payout.php
の95行目付近にそれらしいものが見つかる
$aData['percentage'] = ( 100 / $iRoundShares ) * $aData['valid']; $aData['payout'] = ( $aData['percentage'] / 100 ) * $dReward;
100 / $iRoundShares
に$aData['valid']
を掛け算して出てきたpercentage
とdReward
の積がpayout
になる、といったところ不明な変数は3つ、
$iRoundShares
,$aData['valid']
,$dReward
$dReward
の記述を探す
proportional_payout.php
の71行目に以下の記述
$config['reward_type'] == 'block' ? $dReward = $aBlock['amount'] : $dReward = $config['reward'];
普通のifで書き直したのはこちら
if(config['reward_type'] == 'block'){ $dReward = $aBlock['amount'] }else{ $dReward = $config['reward']; }
config['reward_type']
はinclude/config/global.inc.dist.php
(実稼働環境ではglobal.inc.phpになるだろうが)の244行目に$config['reward_type'] = 'block'
の記述ありつまり
$dReward
には$aBlock['amount']
が入るさて
$aBlock['amount']
はどこで決まる?→49行目のforeachで$aBlock
が出て来る、その元は$aAllBlocks
→35行目で$block->getAllUnaccounted('ASC');
で得られている
block->getAllUnaccounted()
を読む
- その定義は
include/classes/block.class.php
の96行目~ 97行目のクエリは「
blocks
テーブルからaccounted
が0のレコードを取得してくる」ものblocks
テーブルに並んでいるのはプールで発見したブロックたちのように見えるデータベースにてdesc blocksすると、accountedカラムのDefault値が0なことがわかる→値を設定せずINSERTされれば0になる
block.class.php
内のINSERTを探すとaddBlock($block)
が見つかるクエリを眺めるといかにもブロック発見時の動作に見えるが、利用シーンを探して検証する
addBlock()
が呼ばれているところを探してみる
- リポジトリ内検索をすると
cronjobs/findblock.php
に1つあるのみ(82行目で$aData
を引数に呼ばれている) $aData
はどこで得られるか?→53行目のforeachで出て来る、その元は$aTransactions['transactions']
$aTransactions
は41行目の$bitcoin->listsinceblock($strLastBlockHash)
で得られているlistsinceblock
は置いておいて、$strLastBlockHash
を先に見てみる$strLastBlockHash
は30行目で$aLastBlock['blockhash']
が代入されている$aLastBlock
は29行目で$block->getLastValid()
が代入されているinclude/classes/block.class.php
の24行目~getLastValid()
の定義があるそこにあるクエリは「blocksテーブルからconfirmationsが-1よりも大きいレコードでheightが最も高いものを1つ取得してくる」もの(つまり直近に見つかったブロックを取得する)
…つまり
$bitcoin->listsinceblock($strLastBlockHash)
は直近に発見したブロックのblockhashを引数に使う事になるlistsinceblock()
をリポジトリ内検索してみても、ヒットは無いPHPには、存在しないメソッドを呼び出した場合の動作を決めるマジックメソッド
__call
があるので、それを使っているのでは?block.class.php
内には__call
は見当たらないので、requireしてるinclude/lib/jsonRPCClient.php
を見てみると83行目からそれらしい記述があるcoindに喋りかけてそうな感じに見えるが、それも確認してみる
パラメータの流れを見ると
$url
あたりから全部来てる→__construct
で$url
が代入されている$url
はコンストラクタの引数なので、それを利用するシーンを探すbitcoin.class.php
の241行目からjsonRPCClient
クラスを継承したBitcoinClient
クラスの定義で、基底クラスのコンストラクタは278行目で実行される→その引数$uri
は277行目で定義されるが、そのデータはBitcoinClient
のコンストラクタの引数から与えられるBitcoinClient
をリポジトリ内検索すると、include/classes/bitcoinwrapper.class.php
で定義されるBitcoinWrapper
が見つかる(これはBitcoinClient
を継承するクラス)bitcoinwrapper.class.php
の112行目でインスタンスの生成が行われており、そのパラメータ$config['wallet']['username']
や$config['wallet']['password']
等はinclude/config/global.inc.dis.php
の70行目付近で設定されているこのパラメータはウォレットにアクセスする情報なので、やはり
include/lib/jsonRPCClient.php
の83行目~はcoindに喋りかけているようだcoindのコマンドとしての
listsinceblock
は「指定ブロック以降の(ウォレットに影響を与えた)トランザクションを取得」つまり
$aTransactions
は「直近に見つかったブロック以降の、ウォレットに影響を与えるトランザクション全て」になる要は、addBlockの引数は「直近に発見したブロック以降ウォレットに影響を与えたトランザクション」であって、blocksテーブルは発見したブロック群であると思われる
結局$dReward
は何が入るか
- 発見したブロック群からUnaccountedなレコードのみを取り出してきたのが
$aAllBlocks
である(UnaccountedはMPOSが未だ処理していないという意味と思われる) $aAllBlocks
のうちの1つ…のamountが$dReward
に代入される
一旦休憩、まとめ……
- そろそろ忘れてるのでまとめると、
$aData['percentage'] = ( 100 / $iRoundShares ) * $aData['valid'] $aData['payout'] = ( $aData['percentage'] / 100 ) * $dReward
- で報酬が決められているのでは?と思う
$dReward
は「直近に発見したブロック以降で、ウォレットに影響を与えたトランザクションたち」のうちの1つ、のamountが入るつまり
$dReward
は「発見したブロックで得られた報酬の量」である残りの不明な変数は
$iRoundShares
,$aData['valid']
の2つ
$aData['valid']
には何が入っている?
$aData
はcronjobs/proportional_payout.php
の79行目で$aAccountShares
から得られる事がわかるその次のところに
Skip users with only invalids
とあるので、恐らく各ユーザのValidなShareの量である$aAccountShares
は69行目で$share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']);
が代入されているinclude/classes/share.class.php
の96行目~getSharesForAccounts
の定義があるそこにあるクエリは、
SELECT a.id, SUBSTRING_INDEX( s.username , '.', 1 ) as username, a.no_fees AS no_fees, IFNULL(SUM(IF(our_result='Y', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) AS valid, IFNULL(SUM(IF(our_result='N', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) AS invalid FROM $this->table AS s LEFT JOIN " . $this->user->getTableName() . " AS a ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) WHERE s.id > ? AND s.id <= ? AND a.is_locked != 2 GROUP BY username DESC
- ややこしいのでFROM句から見ていく。それぞれ変数やメソッドから出力されたものに書き直すと以下のような状態
FROM shares AS s LEFT JOIN accounts AS a ON a.username = SUBSTRING_INDEX( s.username, '.', 1 )
- sharesテーブルはユーザが提出したShareの記録、accountsテーブルはユーザ一覧
sharesにおけるusernameは
ユーザ名.ワーカ名
であって、accountsにおけるusernameはユーザ名
であることに注意すれば、accountsに存在するユーザが提出したShareたちを取得しようとしている事がわかるWHERE句は
s.id > $previous_upstream AND s.id <= $current_upstream AND a.is_locked !=2
という事らしい最後はロックされているかのチェックぽいので良いとして、もう一つは(まとめて書くと)
$previous_upstream < s.id <= $current_upstream
であるが、その変数は呼び出し元ではどう扱われているか?proportional_payout.php
の67行目に$iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0;
の記述がある$iIndex
は$aAllBlocks
をforeachしたときのインデックスである(49行目)発見したブロック群のうち、Unaccountedなものである
$aAllBlocks
のうち、1つ前のshare_idが前述の不等式において$previous_upstream
で使われる(share_idがfalseの場合には0が使われる)$current_upstream
は$aBlock['share_id']
が使われる(今処理しているブロックのshare_idが使われる)つまり「1つ前のブロック発見~今のブロックまでに提出された範囲内に絞る」という意味になるようだ
最後に、SELECTするカラムのうち3つは、
a.id, SUBSTRING_INDEX( s.username, '.', 1 ) as username, a.no_fees AS no_fees
で、大変簡単で、それぞれアカウントID(ユーザ名でなくデータベース上のID)、ユーザ名、feeの状況。valid, invalidについては、適宜並べてみると、以下のようになる
IFNULL( SUM( IF( our_result='Y', IF( s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty ), 0 ) ), 0 ) AS valid
- まずSUMから読んでいくと、
our_result='Y'
の時に最深のIF()を、our_result='N'
の時0を足し算していく 最深のIF()は、各Shareレコードのdifficultyが0だったら
2^($this->config['difficulty'] - 16)
を、0でなかったらdificultyをそのまま返す($this->config['difficulty']
はinclude/config/global.inc.dist.php
の237行目で定義されている)SUMがNULLだった場合には、0を返す
invalidではour_resultのY, Nが逆になっている
最後に
GROUP BY username
があるので、各ユーザごとにまとまるようだつまりは、各ユーザが提出したShareのDifficultyのそれぞれの総和を返すものと思えば良い
そして
shares.difficulty
はワーカに割り振られたdifficultyの値らしい(SELECT difficulty FROM shares WHERE username='てきとうなワーカ名' ORDER BY id DESC LIMIT 1;
で得られた値と、マイナー側に示されているDiffが一致しているので)
$iRoundShares
には何が入っている?
cronjobs/proportional_payout.php
の70行目で$share->getRoundShares($iPreviousShareId, $aBlock['share_id']);
が代入されているgetRoundShares
という名前から察せられるが、当該ラウンドのShare量が返されているのであろうクエリの
getSharesForAccounts
との差は、GROUP BY句が無い事、our_result='Y'
以外は無視する事
不明だった変数をまとめると
$iRoundShares
:当該ラウンドにおいて全ユーザが提出したSharesのDifficultyの総和$aData['valid']
:当該ラウンドにおいて各ユーザが提出したSharesのDifficultyの総和$dReward
:当該ブロックにてcoindが得た報酬
これらを踏まえて最初の式を見ると
$aData['percentage'] = ( 100 / $iRoundShares ) * $aData['valid'] $aData['payout'] = ( $aData['percentage'] / 100 ) * $dReward
- てきとうに代入して書き換えてみると、
$aData['payout'] = ( $aData['valid'] / $iRoundShares ) * $dReward
- つまり、ブロック報酬
$dReward
をラウンド内のSharesの割合で分配する、その割合の計算元は、SharesのDifficultyの総和である