初のAdvent Calendar参加です。1日遅れですが、暗号通貨Advent Calendar 5日目に登録しました。
お茶漬けと申します。普段は都内で学生やってます。研究進まない。
昨日4日目は@zinntikumugai氏の仮想通貨を変換した時の金額を求めるTwitterBOT作ったお話でした。JPYやBTCを挟まずに、目的の通貨へのレートを一発で教えてもらえるTwitterBOTだそうです。中間に基準通貨のようなものを挟まずともレートを教えてくれるというのは、便利で良さそうです。新たに対応通貨を増やす等、精力的に活動しているようで、今後が楽しみですね。
さて、プールマイニングを行った場合に、報酬はShareの比率に従って分配されるとかなんとか言いますが、その比率とは何を元に算出されるのか、きちんと考えたことが無かったので、具体的に理解する事が目標です。報酬の分配方式としては、代表的なものにPPLNSがあると思いますが、ここではPROPのみに絞ります(単純に私の技量と気力が足りなかったので)。また、実装を見させてもらうのは、MPOS/php-mposです。
基本的にはgithubのソースを眺めて追いかけていきますが、随時データベースの中身も利用します(採掘経験のある、mposデータベースが手元にあるので)。
ちなみに私は、きちんとPHPを触ったことがない上に、人のコードを読む経験が非常に少ないので、かなり手探りになります。ご了承ください。また、間違い等あったらご指摘頂けると大変嬉しく思います。
比率の計算は各ユーザの提出したShare達のDifficultyの総和を元に行う。おしまい。
以下はphp初心者が手探りで読んでいった痕跡です。
/cronjobs/run-crons.shをcronで回すので、それを見てみる[bash]
~略~
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
[/bash]
proportional_payout.phpとかいうドンピシャくさいものがあるproportional_payout.phpを読んでみるcreditで検索してみると、122行目にAdd new credit transactionという文言がある
そこの周辺はこんな感じ
[php]
// 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());
[/php]
「$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行目付近にそれらしいものが見つかる
[php]
$aData[‘percentage’] = ( 100 / $iRoundShares ) * $aData[‘valid’];
$aData[‘payout’] = ( $aData[‘percentage’] / 100 ) * $dReward;
[/php]
100 / $iRoundSharesに$aData['valid']を掛け算して出てきたpercentageとdRewardの積がpayoutになる、といったところ
不明な変数は3つ、$iRoundShares, $aData['valid'], $dReward
$dRewardの記述を探すproportional_payout.phpの71行目に以下の記述[php]
$config[‘reward_type’] == ‘block’ ? $dReward = $aBlock[‘amount’] : $dReward = $config[‘reward’];
[/php]
普通のifで書き直したのはこちら
[php]
if(config[‘reward_type’] == ‘block’){
$dReward = $aBlock[‘amount’]
}else{
$dReward = $config[‘reward’];
}
[/php]
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に代入される
[php]
$aData[‘percentage’] = ( 100 / $iRoundShares ) * $aData[‘valid’]
$aData[‘payout’] = ( $aData[‘percentage’] / 100 ) * $dReward
[/php]
で報酬が決められているのでは?と思う
$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の定義がある
そこにあるクエリは、
[sql]
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
[/sql]
[sql]
FROM shares AS s
LEFT JOIN accounts AS a
ON a.username = SUBSTRING_INDEX( s.username, ‘.’, 1 )
[/sql]
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については、適宜並べてみると、以下のようになる
[sql]
IFNULL(
SUM(
IF(
our_result=’Y’,
IF(
s.difficulty=0,
POW(2, (“ . $this->config[‘difficulty’] . “ - 16)),
s.difficulty
),
0
)
),
0
)
AS valid
[/sql]
まず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が得た報酬
[php]
$aData[‘percentage’] = ( 100 / $iRoundShares ) * $aData[‘valid’]
$aData[‘payout’] = ( $aData[‘percentage’] / 100 ) * $dReward
[/php]
[php]
$aData[‘payout’] = ( $aData[‘valid’] / $iRoundShares ) * $dReward
[/php]
$dRewardをラウンド内のSharesの割合で分配する、その割合の計算元は、SharesのDifficultyの総和である