First working version of PPLNS payouts

* Based PPLNS on Prop Payout script
* Using defaults from prop payout, no class adjustments
* Added more methods required for PPLNS
* Added block methods for dynamic payout calculations
* Added PPLNS Sidebar that also displays the PPLNS Target
 * Shares beyond this target will not be included in payouts
 * Shares missing to this target will be added from archives
* Enabled archiving by default for PPLNS
* Added configuration options for PPLNS
 * Documented the usage for PPLNS, defaults are sane
* Added pplns_payout to run-crons

Addresses #143 and if accepted will fix it
This commit is contained in:
Sebastian Grewe 2013-07-03 18:57:36 +02:00
parent dc51d874a7
commit 2f2acdad6d
9 changed files with 399 additions and 43 deletions

View File

@ -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

142
cronjobs/pplns_payout.php Executable file
View File

@ -0,0 +1,142 @@
#!/usr/bin/php
<?php
/*
Copyright:: 2013, Sebastian Grewe
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Include all settings and classes
require_once('shared.inc.php');
// Check if we are set as the payout system
if ($config['payout_system'] != 'pplns') {
verbose("Please activate this cron in configuration via payout_system = pplns\n");
exit(0);
}
// Fetch all unaccounted blocks
$aAllBlocks = $block->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");
}
}

View File

@ -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"

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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:

View File

@ -0,0 +1,74 @@
<div class="block" style="clear:none; margin-top:15px; margin-left:13px;">
<div class="block_head">
<div class="bheadl"></div>
<div class="bheadr"></div>
<h1>Dashboard</h1>
</div>
<div class="block_content" style="padding-top:10px;">
<table class="sidebar" style="width: 196px">
<tr>
<td><b>PPLNS Target</b></td>
<td class="right">{$GLOBAL.pplns.target|number_format}</td>
</tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr>
<td colspan="2"><b><u>Your Stats</u></b></td>
</tr>
<tr>
<td><b>Hashrate</b></td>
<td class="right">{$GLOBAL.userdata.hashrate|number_format} KH/s</td>
</tr>
<tr>
<td colspan="2"><b><u>Unpaid Shares</u></b> <span id='tt'><img src='{$PATH}/images/questionmark.png' height='15px' width='15px' title='Submitted shares between the last 120 confirms block until now.'></span></td>
</tr>
<tr>
<td><b>Your Valid<b></td>
<td class="right"><i>{$GLOBAL.userdata.shares.valid|number_format}</i><font size='1px'></font></b></td>
</tr>
<tr>
<td><b>Pool Valid</td>
<td class="right"><i>{$GLOBAL.roundshares.valid|number_format}</i> <font size='1px'></font></b></td>
</tr>
<tr>
<td colspan="2"><b><u>Round Shares</u></b> <span id='tt'><img src='{$PATH}/images/questionmark.png' height='15px' width='15px' title='Submitted shares since last found block (ie. round shares)'></span></td>
</tr>
<tr>
<td><b>Pool Valid</b></td>
<td class="right"><i>{$GLOBAL.roundshares.valid|number_format}</i></td>
</tr>
<tr>
<td><b>Pool Invalid</b></td>
<td class="right"><i>{$GLOBAL.roundshares.invalid|number_format}</i>{if $GLOBAL.roundshares.valid > 0}<font size='1px'> ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid)|number_format:"2"}%)</font>{/if}</td>
</tr>
<tr>
<td><b>Your Invalid</b></td>
<td class="right"><i>{$GLOBAL.userdata.shares.invalid|number_format}</i>{if $GLOBAL.roundshares.valid > 0}<font size='1px'> ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid)|number_format:"2"}%)</font>{/if}</td>
</tr>
<tr>
<td colspan="2"><b><u>{$GLOBAL.config.currency} Round Estimate</u></b></td>
</tr>
<tr>
<td><b>Block</b></td>
<td class="right">{$GLOBAL.userdata.est_block|number_format:"3"}</td>
</tr>
<tr>
<td><b>Fees</b></td>
<td class="right">{$GLOBAL.userdata.est_fee|number_format:"3"}</td>
</tr>
<tr>
<td><b>Donation</b></td>
<td class="right">{$GLOBAL.userdata.est_donation|number_format:"3"}</td>
</tr>
<tr>
<td><b>Payout</b></td>
<td class="right">{$GLOBAL.userdata.est_payout|number_format:"3"}</td>
</tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr><td colspan="2"><b><u>{$GLOBAL.config.currency} Account Balance</u></b></td></tr>
<tr><td>Confirmed</td><td class="right"><b>{$GLOBAL.userdata.balance.confirmed|default:"0"}</td></tr>
<tr><td>Unconfirmed</td><td class="right"><b>{$GLOBAL.userdata.balance.unconfirmed|default:"0"}</td></tr>
</table>
</div>
<div class="bendl"></div>
<div class="bendr"></div>
</div>

View File

@ -0,0 +1,63 @@
<table>
<tr>
<td><b>PPLNS Target</b></td>
<td align="right">{$GLOBAL.pplns.target|number_format}</td>
</tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr>
<td colspan="2"><b><u>Your Stats</u></b></td>
</tr>
<tr>
<td><b>Hashrate</b></td>
<td align="right">{$GLOBAL.userdata.hashrate|number_format} KH/s</td>
</tr>
<tr>
<td colspan="2"><b><u>Unpaid Shares</u></b> <span id='tt'><img src='{$PATH}/images/questionmark.png' height='15px' width='15px' title='Submitted shares between the last 120 confirms block until now.'></span></td>
</tr>
<tr>
<td><b>Your Valid<b></td>
<td align="right"><i>{$GLOBAL.userdata.shares.valid|number_format}</i><font size='1px'></font></b></td>
</tr>
<tr>
<td><b>Pool Valid</td>
<td align="right"><i>{$GLOBAL.roundshares.valid|number_format}</i> <font size='1px'></font></b></td>
</tr>
<tr>
<td colspan="2"><b><u>Round Shares</u></b> <span id='tt'><img src='{$PATH}/images/questionmark.png' height='15px' width='15px' title='Submitted shares since last found block (ie. round shares)'></span></td>
</tr>
<tr>
<td><b>Pool Valid</b></td>
<td align="right"><i>{$GLOBAL.roundshares.valid|number_format}</i></td>
</tr>
<tr>
<td><b>Pool Invalid</b></td>
<td align="right"><i>{$GLOBAL.roundshares.invalid|number_format}</i>{if $GLOBAL.roundshares.valid > 0}<font size='1px'> ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid)|number_format:"2"}%)</font>{/if}</td>
</tr>
<tr>
<td><b>Your Invalid</b></td>
<td align="right"><i>{$GLOBAL.userdata.shares.invalid|number_format}</i>{if $GLOBAL.roundshares.valid > 0}<font size='1px'> ({(100 / $GLOBAL.roundshares.valid * $GLOBAL.userdata.shares.invalid)|number_format:"2"}%)</font>{/if}</td>
</tr>
<tr>
<td colspan="2"><b><u>{$GLOBAL.config.currency} Round Estimate</u></b></td>
</tr>
<tr>
<td><b>Block</b></td>
<td align="right">{$GLOBAL.userdata.est_block|number_format:"3"}</td>
</tr>
<tr>
<td><b>Fees</b></td>
<td align="right">{$GLOBAL.userdata.est_fee|number_format:"3"}</td>
</tr>
<tr>
<td><b>Donation</b></td>
<td align="right">{$GLOBAL.userdata.est_donation|number_format:"3"}</td>
</tr>
<tr>
<td><b>Payout</b></td>
<td align="right">{$GLOBAL.userdata.est_payout|number_format:"3"}</td>
</tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr><td colspan="2"><b><u>{$GLOBAL.config.currency} Account Balance</u></b></td></tr>
<tr><td>Confirmed</td><td align="right"><b>{$GLOBAL.userdata.balance.confirmed|default:"0"}</td></tr>
<tr><td>Unconfirmed</td><td align="right"><b>{$GLOBAL.userdata.balance.unconfirmed|default:"0"}</td></tr>
</table>