diff --git a/README.md b/README.md index 14526524..885c5573 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,11 @@ Features The following feature have been implemented so far: -* Mobile WebUI **NEW** +* Mobile WebUI * Reward Systems * Propotional * PPS - * (Planned) PPLNS + * PPLNS **NEW** * Use of memcache for statistics instead of a cronjob * Web User accounts * Re-Captcha protected registration form diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php new file mode 100755 index 00000000..295702c9 --- /dev/null +++ b/cronjobs/pplns_payout.php @@ -0,0 +1,142 @@ +#!/usr/bin/php +getAllUnaccounted('ASC'); +if (empty($aAllBlocks)) { + verbose("No new unaccounted blocks found\n"); + exit(0); +} + +// We support some dynamic share targets but fall back to our fixed value +if ($config['pplns']['shares']['type'] == 'blockavg' && $block->getBlockCount() > 0) { + $pplns_target = round($block->getAvgBlockShares($config['pplns']['type']['blockavg']['blockcount'])); +} else { + $pplns_target = $config['pplns']['shares']['default'] ; +} + +$count = 0; +foreach ($aAllBlocks as $iIndex => $aBlock) { + if (!$aBlock['accounted']) { + $iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; + $iCurrentUpstreamId = $aBlock['share_id']; + $iRoundShares = $share->getRoundShares($iPreviousShareId, $aBlock['share_id']); + $config['reward_type'] == 'block' ? $dReward = $aBlock['amount'] : $dReward = $config['reward']; + $aRoundAccountShares = $share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']); + + if ($iRoundShares >= $pplns_target) { + verbose("Matching or exceeding PPLNS target of $pplns_target\n"); + $aAccountShares = $share->getSharesForAccounts($aBlock['share_id'] - $pplns_target + 1, $aBlock['share_id']); + } else { + verbose("Not able to match PPLNS target of $pplns_target\n"); + // We need to fill up with archived shares + // Grab the full current round shares since we didn't match target + $aAccountShares = $aRoundAccountShares; + // Grab only the most recent shares from Archive that fill the missing shares + $aArchiveShares = $share->getArchiveShares($share->getMaxArchiveShareId() - ($pplns_target- $iRoundShares) + 1, $share->getMaxArchiveShareId()); + // Add archived shares to users current shares, if we have any in archive + if (is_array($aArchiveShares)) { + foreach($aAccountShares as $key => $aData) { + if (array_key_exists($aData['username'], $aArchiveShares)) { + $aAccountShares[$key]['valid'] += $aArchiveShares[$aData['username']]['valid']; + $aAccountShares[$key]['invalid'] += $aArchiveShares[$aData['username']]['invalid']; + } + } + } + } + if (empty($aAccountShares)) { + verbose("\nNo shares found for this block\n\n"); + sleep(2); + continue; + } + + // Table header for account shares + verbose("ID\tUsername\tValid\tInvalid\tPercentage\tPayout\t\tDonation\tFee\t\tStatus\n"); + + // Loop through all accounts that have found shares for this round + foreach ($aAccountShares as $key => $aData) { + // Payout based on PPLNS target shares, proportional payout for all users + $aData['percentage'] = number_format(round(( 100 / $pplns_target) * $aData['valid'], 8), 8); + $aData['payout'] = number_format(round(( $aData['percentage'] / 100 ) * $dReward, 8), 8); + // Defaults + $aData['fee' ] = 0; + $aData['donation'] = 0; + + if ($config['fees'] > 0) + $aData['fee'] = number_format(round($config['fees'] / 100 * $aData['payout'], 8), 8); + // Calculate donation amount, fees not included + $aData['donation'] = number_format(round($user->getDonatePercent($user->getUserId($aData['username'])) / 100 * ( $aData['payout'] - $aData['fee']), 8), 8); + + // Verbose output of this users calculations + verbose($aData['id'] . "\t" . + $aData['username'] . "\t" . + $aData['valid'] . "\t" . + $aData['invalid'] . "\t" . + $aData['percentage'] . "\t" . + $aData['payout'] . "\t" . + $aData['donation'] . "\t" . + $aData['fee'] . "\t"); + + $strStatus = "OK"; + // Add full round share statistics, not just PPLNS + foreach ($aRoundAccountShares as $key => $aRoundData) { + if ($aRoundData['username'] == $aData['username']) + if (!$statistics->updateShareStatistics($aRoundData, $aBlock['id'])) + $strStatus = "Stats Failed"; + } + // Add new credit transaction + if (!$transaction->addTransaction($aData['id'], $aData['payout'], 'Credit', $aBlock['id'])) + $strStatus = "Transaction Failed"; + // Add new fee debit for this block + if ($aData['fee'] > 0 && $config['fees'] > 0) + if (!$transaction->addTransaction($aData['id'], $aData['fee'], 'Fee', $aBlock['id'])) + $strStatus = "Fee Failed"; + // Add new donation debit + if ($aData['donation'] > 0) + if (!$transaction->addTransaction($aData['id'], $aData['donation'], 'Donation', $aBlock['id'])) + $strStatus = "Donation Failed"; + verbose("\t$strStatus\n"); + } + + // Move counted shares to archive before this blockhash upstream share + $share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId); + // Delete all accounted shares + if (!$share->deleteAccountedShares($iCurrentUpstreamId, $iPreviousShareId)) { + verbose("\nERROR : Failed to delete accounted shares from $iPreviousShareId to $iCurrentUpstreamId, aborting!\n"); + exit(1); + } + // Mark this block as accounted for + if (!$block->setAccounted($aBlock['id'])) { + verbose("\nERROR : Failed to mark block as accounted! Aborting!\n"); + } + + verbose("------------------------------------------------------------------------\n\n"); + } +} diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index c9dfd949..b6b241b4 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -16,7 +16,7 @@ PIDFILE='/tmp/mmcfe-ng-cron.pid' CRONHOME='.' # List of cruns to execute -CRONS="findblock.php proportional_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php" +CRONS="findblock.php proportional_payout.php pplns_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php" # Additional arguments to pass to cronjobs CRONARGS="-v" diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index 81d1806a..8f52efd9 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -79,6 +79,30 @@ class Block { return false; } + /** + * Get total amount of blocks in our table + * @param noone + * @return data int Count of rows + **/ + public function getBlockCount() { + $stmt = $this->mysqli->prepare("SELECT COUNT(id) AS blocks FROM $this->table"); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return (int)$result->fetch_object()->blocks; + return false; + } + + /** + * Fetch our average share count for the past N blocks + * @param limit int Maximum blocks to check + * @return data float Float value of average shares + **/ + public function getAvgBlockShares($limit=10) { + $stmt = $this->mysqli->prepare("SELECT AVG(shares) AS average FROM $this->table LIMIT ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $limit) && $stmt->execute() && $result = $stmt->get_result()) + return (float)$result->fetch_object()->average; + return false; + } + /** * Fetch all unconfirmed blocks from table * @param confirmations int Required confirmations to consider block confirmed diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index 496652f0..ed6078ea 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -13,9 +13,10 @@ class Share { // This defines each share public $rem_host, $username, $our_result, $upstream_result, $reason, $solution, $time; - public function __construct($debug, $mysqli, $salt) { + public function __construct($debug, $mysqli, $user) { $this->debug = $debug; $this->mysqli = $mysqli; + $this->user = $user; $this->debug->append("Instantiated Share class", 2); } @@ -86,47 +87,67 @@ class Share { * Fetch all shares grouped by accounts to count share per account * @param previous_upstream int Previous found share accepted by upstream to limit results * @param current_upstream int Current upstream accepted share + * @param limit int Limit to this amount of shares for PPLNS * @return data array username, valid and invalid shares from account **/ public function getSharesForAccounts($previous_upstream=0, $current_upstream) { - $stmt = $this->mysqli->prepare("SELECT - a.id, - validT.account AS username, - sum(validT.valid) as valid, - IFNULL(sum(invalidT.invalid),0) as invalid - FROM - ( - SELECT DISTINCT - SUBSTRING_INDEX( `username` , '.', 1 ) as account, - COUNT(id) AS valid - FROM $this->table - WHERE id BETWEEN ? AND ? - AND our_result = 'Y' - GROUP BY account - ) validT - LEFT JOIN - ( - SELECT DISTINCT - SUBSTRING_INDEX( `username` , '.', 1 ) as account, - COUNT(id) AS invalid - FROM $this->table - WHERE id BETWEEN ? AND ? - AND our_result = 'N' - GROUP BY account - ) invalidT - ON validT.account = invalidT.account - INNER JOIN accounts a ON a.username = validT.account - GROUP BY a.username DESC"); - if ($this->checkStmt($stmt)) { - $stmt->bind_param('iiii', $previous_upstream, $current_upstream, $previous_upstream, $current_upstream); - $stmt->execute(); - $result = $stmt->get_result(); - $stmt->close(); + $stmt = $this->mysqli->prepare(" + SELECT + a.id, + SUBSTRING_INDEX( s.username , '.', 1 ) as username, + SUM(IF(our_result='Y', 1, 0)) AS valid, + SUM(IF(our_result='N', 1, 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 BETWEEN ? AND ? + GROUP BY username DESC + "); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); - } return false; } + /** + * Fetch the highest available share ID from archive + **/ + function getMaxArchiveShareId() { + $stmt = $this->mysqli->prepare(" + SELECT MAX(share_id) AS share_id FROM $this->tableArchive + "); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_object()->share_id; + return false; + } + + /** + * We need a certain amount of valid archived shares + * param left int Left/lowest share ID + * param right int Right/highest share ID + * return array data Returns an array with usernames as keys for easy access + **/ + function getArchiveShares($left, $right) { + $stmt = $this->mysqli->prepare(" + SELECT + a.id, + SUBSTRING_INDEX( s.username , '.', 1 ) as username, + SUM(IF(our_result='Y', 1, 0)) AS valid, + SUM(IF(our_result='N', 1, 0)) AS invalid + FROM $this->tableArchive AS s + LEFT JOIN " . $this->user->getTableName() . " AS a + ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) + WHERE s.id BETWEEN ? AND ? + GROUP BY username DESC + "); + if ($this->checkStmt($stmt) && $stmt->bind_param("ii", $left, $right) && $stmt->execute() && $result = $stmt->get_result()) { + $aData = NULL; + while ($row = $result->fetch_assoc()) { + $aData[$row['username']] = $row; + } + if (is_array($aData)) return $aData; + } + return false; + } /** * Move accounted shares to archive table, this step is optional * @param previous_upstream int Previous found share accepted by upstream to limit results @@ -135,10 +156,11 @@ class Share { * @return bool **/ public function moveArchive($current_upstream, $block_id, $previous_upstream=0) { - $archive_stmt = $this->mysqli->prepare("INSERT INTO $this->tableArchive (share_id, username, our_result, upstream_result, block_id, time) - SELECT id, username, our_result, upstream_result, ?, time - FROM $this->table - WHERE id BETWEEN ? AND ?"); + $archive_stmt = $this->mysqli->prepare(" + INSERT INTO $this->tableArchive (share_id, username, our_result, upstream_result, block_id, time) + SELECT id, username, our_result, upstream_result, ?, time + FROM $this->table + WHERE id BETWEEN ? AND ?"); if ($this->checkStmt($archive_stmt) && $archive_stmt->bind_param('iii', $block_id, $previous_upstream, $current_upstream) && $archive_stmt->execute()) { $archive_stmt->close(); return true; @@ -267,4 +289,4 @@ class Share { } } -$share = new Share($debug, $mysqli, SALT); +$share = new Share($debug, $mysqli, $user); diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 9390b534..995fc0b2 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -183,6 +183,30 @@ $config['chaininfo'] = 'http://allchains.info'; // Pool fees applied to users in percent, default: 0 (disabled) $config['fees'] = 0; +/** + * PPLNS requires some settings to run properly. First we need to define + * a default shares count that is applied if we don't have a proper type set. + * Different dynamic types can be applied, or you can run a fixed scheme. + * + * Explanation + * default : Default target shares for PPLNS + * type : Payout type used in PPLNS + * blockcount : Amount of blocks to check for avg shares + * + * Available Options: + * default : amount of shares, integeger + * type : blockavg or fixed + * blockcount : amount of blocks, any integer + * + * Defaults: + * default = 4000000 + * type = `blockavg` + * blockcount = 10 + **/ +$config['pplns']['shares']['default'] = 4000000; +$config['pplns']['shares']['type'] = 'blockavg'; +$config['pplns']['blockavg']['blockcount'] = 10; + // Pool target difficulty as set in pushpoold configuration file // Please also read this for stratum: https://github.com/TheSerapher/php-mmcfe-ng/wiki/FAQ $config['difficulty'] = 20; diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index 147b38e9..fd5083d6 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -83,6 +83,13 @@ if (@$_SESSION['USERDATA']['id']) { $aGlobal['userdata']['sharerate'] = $statistics->getUserSharerate($_SESSION['USERDATA']['id']); switch ($config['payout_system']) { + case 'pplns': + if ($iAvgBlockShares = round($block->getAvgBlockShares($config['pplns']['type']['blockavg']['blockcount']))) { + $aGlobal['pplns']['target'] = $iAvgBlockShares; + } else { + $aGlobal['pplns']['target'] = $config['pplns']['shares']['default']; + } + break; case 'pps': break; default: diff --git a/public/templates/mmcFE/global/sidebar_pplns.tpl b/public/templates/mmcFE/global/sidebar_pplns.tpl new file mode 100644 index 00000000..deb0c1cc --- /dev/null +++ b/public/templates/mmcFE/global/sidebar_pplns.tpl @@ -0,0 +1,74 @@ +
+
+
+
+

Dashboard

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/public/templates/mobile/global/sidebar_pplns.tpl b/public/templates/mobile/global/sidebar_pplns.tpl new file mode 100644 index 00000000..95dcf93c --- /dev/null +++ b/public/templates/mobile/global/sidebar_pplns.tpl @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PPLNS Target{$GLOBAL.pplns.target|number_format}
 
Your Stats
Hashrate{$GLOBAL.userdata.hashrate|number_format} KH/s
Unpaid Shares
Your Valid{$GLOBAL.userdata.shares.valid|number_format}
Pool Valid{$GLOBAL.roundshares.valid|number_format}
Round Shares
Pool Valid{$GLOBAL.roundshares.valid|number_format}
Pool Invalid{$GLOBAL.roundshares.invalid|number_format}{if $GLOBAL.roundshares.valid > 0} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid)|number_format:"2"}%){/if}
Your Invalid{$GLOBAL.userdata.shares.invalid|number_format}{if $GLOBAL.roundshares.valid > 0} ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid)|number_format:"2"}%){/if}
{$GLOBAL.config.currency} Round Estimate
Block{$GLOBAL.userdata.est_block|number_format:"3"}
Fees{$GLOBAL.userdata.est_fee|number_format:"3"}
Donation{$GLOBAL.userdata.est_donation|number_format:"3"}
Payout{$GLOBAL.userdata.est_payout|number_format:"3"}
 
{$GLOBAL.config.currency} Account Balance
Confirmed{$GLOBAL.userdata.balance.confirmed|default:"0"}
Unconfirmed{$GLOBAL.userdata.balance.unconfirmed|default:"0"}