php-mpos/public/include/classes/share.class.php
Sebastian Grewe 35ab40cbb7 [FIX] Inifite loop on archive_cleanup
Fixes an infinite loop in cleaning up the archive table:

* Shares come in faster than defined in cleanup sleep timeout

Will use an upper limit ID which would match the delete query at the
initial state. Skips archiving if no ID is found (e.g. no rows match the
archive purge conditions).

This will fix #997 once merged.
2013-12-16 16:50:39 +01:00

431 lines
19 KiB
PHP

<?php
// Make sure we are called from index.php
if (!defined('SECURITY'))
die('Hacking attempt');
class Share Extends Base {
protected $table = 'shares';
protected $tableArchive = 'shares_archive';
private $oUpstream;
private $iLastUpstreamId;
// This defines each share
public $rem_host, $username, $our_result, $upstream_result, $reason, $solution, $time, $difficulty;
/**
* Fetch archive tables name for this class
* @param none
* @return data string Table name
**/
public function getArchiveTableName() {
return $this->tableArchive;
}
/**
* Fetch a single share by ID
* @param id int Share ID
* @return array Share data
**/
public function getShareById($id) {
return $this->getAllAssoc($id);
}
/**
* Update an entire shares data
**/
public function updateShareById($id, $data) {
$this->debug->append("STA " . __METHOD__, 4);
$sql = "UPDATE $this->table SET";
$start = true;
// Remove ID column
unset($data['id']);
foreach ($data as $column => $value) {
$start == true ? $sql .= " $column = ? " : $sql .= ", $column = ?";
$start = false;
switch($column) {
case 'difficulty':
$this->addParam('d', $value);
break;
default:
$this->addParam('s', $value);
break;
}
}
$sql .= " WHERE id = ? LIMIT 1";
$this->addParam('i', $id);
$stmt = $this->mysqli->prepare($sql);
if ($this->checkStmt($stmt) && call_user_func_array( array($stmt, 'bind_param'), $this->getParam()) && $stmt->execute())
return true;
return $this->sqlError();
}
/**
* Get last inserted Share ID from Database
* Used for PPS calculations without moving to archive
**/
public function getLastInsertedShareId() {
$stmt = $this->mysqli->prepare("SELECT MAX(id) AS id FROM $this->table");
if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result())
return $result->fetch_object()->id;
return $this->sqlError();
}
/**
* Get all valid shares for this round
* @param previous_upstream int Previous found share accepted by upstream to limit results
* @param current_upstream int Current upstream accepted share
* @return data int Total amount of counted shares
**/
public function getRoundShares($previous_upstream=0, $current_upstream) {
$stmt = $this->mysqli->prepare("SELECT
ROUND(IFNULL(SUM(IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 8) AS total
FROM $this->table
WHERE our_result = 'Y'
AND id > ? AND id <= ?
");
if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute() && $result = $stmt->get_result())
return $result->fetch_object()->total;
return $this->sqlError();
}
/**
* 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,
SUBSTRING_INDEX( s.username , '.', 1 ) as username,
a.no_fees AS no_fees,
ROUND(IFNULL(SUM(IF(our_result='Y', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 8) AS valid,
ROUND(IFNULL(SUM(IF(our_result='N', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 8) 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 <= ?
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 $this->sqlError();
}
/**
* Fetch the highest available share ID
**/
function getMaxShareId() {
$stmt = $this->mysqli->prepare("SELECT MAX(id) AS id FROM $this->table");
if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result())
return $result->fetch_object()->id;
return $this->sqlError();
}
/**
* 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 $this->sqlError();
}
/**
* 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($iCount) {
$iMinId = $this->getMinArchiveShareId($iCount);
$iMaxId = $this->getMaxArchiveShareId();
$stmt = $this->mysqli->prepare("
SELECT
a.id,
SUBSTRING_INDEX( s.username , '.', 1 ) as account,
a.no_fees AS no_fees,
ROUND(IFNULL(SUM(IF(our_result='Y', IF(s.difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 8) AS valid,
ROUND(IFNULL(SUM(IF(our_result='N', IF(s.difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 8) AS invalid
FROM $this->tableArchive AS s
LEFT JOIN " . $this->user->getTableName() . " AS a
ON a.username = SUBSTRING_INDEX( s.username , '.', 1 )
WHERE s.share_id > ? AND s.share_id <= ?
GROUP BY account DESC");
if ($this->checkStmt($stmt) && $stmt->bind_param("ii", $iMinId, $iMaxId) && $stmt->execute() && $result = $stmt->get_result()) {
$aData = NULL;
while ($row = $result->fetch_assoc()) {
$aData[$row['account']] = $row;
}
if (is_array($aData)) return $aData;
}
return $this->sqlError();
}
/**
* We keep shares only up to a certain point
* This can be configured by the user.
* @return return bool true or false
**/
public function purgeArchive() {
// Fallbacks if unset
if (!isset($this->config['purge']['shares'])) $this->config['purge']['shares'] = 25000;
if (!isset($this->config['purge']['sleep'])) $this->config['purge']['sleep'] = 1;
// TODO: This could need some cleanup work somtime but works
if ($this->config['payout_system'] == 'pplns') {
// Fetch our last block so we can go back configured rounds
$aLastBlock = $this->block->getLast();
// Fetch the block we need to find the share_id
$aBlock = $this->block->getBlock($aLastBlock['height'] - $this->config['archive']['maxrounds']);
// We need to find a hard limit id so we don't run into an infinite loop, skip process if we can't find a limit
$stmt = $this->mysqli->prepare("SELECT MAX(id) AS id FROM $this->tableArchive WHERE block_id < ? AND time < DATE_SUB(now(), INTERVAL ? MINUTE)");
if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $aBlock['id'], $this->config['archive']['maxage']) && $stmt->execute() && $result = $stmt->get_result())
if ( ! $max_id = $result->fetch_object()->id ) return true;
// Now that we know our block, remove those shares
$affected = 1;
while ($affected > 0) {
// Sleep first to allow any IO to cleanup
sleep($this->config['purge']['sleep']);
$stmt = $this->mysqli->prepare("DELETE FROM $this->tableArchive WHERE block_id < ? AND time < DATE_SUB(now(), INTERVAL ? MINUTE) AND id <= ? LIMIT " . $this->config['purge']['shares']);
if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $aBlock['id'], $this->config['archive']['maxage'], $max_id) && $stmt->execute()) {
$affected = $stmt->affected_rows;
} else {
return $this->sqlError();
}
}
} else {
// We need to find a hard limit id so we don't run into an infinite loop, skip process if we can't find a limit
$stmt = $this->mysqli->prepare("SELECT MAX(id) AS id FROM $this->tableArchive WHERE time < DATE_SUB(now(), INTERVAL ? MINUTE)");
if ($this->checkStmt($stmt) && $stmt->bind_param('i', $this->config['archive']['maxage']) && $stmt->execute() && $result = $stmt->get_result())
if ( ! $max_id = $result->fetch_object()->id ) return true;
$affected = 1;
while ($affected > 0) {
// Sleep first to allow any IO to cleanup
sleep($this->config['purge']['sleep']);
$stmt = $this->mysqli->prepare("DELETE FROM $this->tableArchive WHERE time < DATE_SUB(now(), INTERVAL ? MINUTE) AND id <= ? LIMIT " . $this->config['purge']['shares']);
if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $this->config['archive']['maxage'], $max_id) && $stmt->execute()) {
$affected = $stmt->affected_rows;
} else {
return $this->sqlError();
}
}
}
return true;
}
/**
* Move accounted shares to archive table, this step is optional
* @param previous_upstream int Previous found share accepted by upstream to limit results
* @param current_upstream int Current upstream accepted share
* @param block_id int Block ID to assign shares to a specific block
* @return bool
**/
public function moveArchive($current_upstream, $block_id, $previous_upstream=0) {
if ($this->config['payout_system'] != 'pplns') {
// We don't need archived shares that much, so only archive as much as configured
$sql = "
INSERT INTO $this->tableArchive (share_id, username, our_result, upstream_result, block_id, time, difficulty)
SELECT id, username, our_result, upstream_result, ?, time, IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty) AS difficulty
FROM $this->table
WHERE id > ? AND id <= ?
AND time >= DATE_SUB(now(), INTERVAL " . $this->config['archive']['maxage'] . " MINUTE)";
} else {
// PPLNS needs archived shares for later rounds, so we have to copy them all
$sql = "
INSERT INTO $this->tableArchive (share_id, username, our_result, upstream_result, block_id, time, difficulty)
SELECT id, username, our_result, upstream_result, ?, time, IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty) AS difficulty
FROM $this->table
WHERE id > ? AND id <= ?";
}
$archive_stmt = $this->mysqli->prepare($sql);
if ($this->checkStmt($archive_stmt) && $archive_stmt->bind_param('iii', $block_id, $previous_upstream, $current_upstream) && $archive_stmt->execute())
return true;
return $this->sqlError();
}
/**
* Delete accounted shares from shares table
* @param current_upstream int Current highest upstream ID
* @param previous_upstream int Previous upstream ID
* @return bool true or false
**/
public function deleteAccountedShares($current_upstream, $previous_upstream=0) {
// Fallbacks if unset
if (!isset($this->config['purge']['shares'])) $this->config['purge']['shares'] = 25000;
if (!isset($this->config['purge']['sleep'])) $this->config['purge']['sleep'] = 1;
$affected = 1;
while ($affected > 0) {
// Sleep first to allow any IO to cleanup
sleep($this->config['purge']['sleep']);
$stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE id > ? AND id <= ? LIMIT " . $this->config['purge']['shares']);
$start = microtime(true);
if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute()) {
$affected = $stmt->affected_rows;
} else {
return $this->sqlError();
}
}
return true;
}
/**
* Set/get last found share accepted by upstream: id and accounts
**/
public function setLastUpstreamId() {
$this->iLastUpstreamId = @$this->oUpstream->id ? $this->oUpstream->id : 0;
}
public function getLastUpstreamId() {
return @$this->iLastUpstreamId ? @$this->iLastUpstreamId : 0;
}
public function getUpstreamFinder() {
return @$this->oUpstream->account;
}
public function getUpstreamWorker() {
return @$this->oUpstream->worker;
}
public function getUpstreamShareId() {
return @$this->oUpstream->id;
}
/**
* Find upstream accepted share that should be valid for a specific block
* Assumptions:
* * Shares are matching blocks in ASC order
* * We can skip all upstream shares of previously found ones used in a block
* @param last int Skips all shares up to last to find new share
* @return bool
**/
public function findUpstreamShare($aBlock, $last=0) {
// Many use stratum, so we create our stratum check first
$version = pack("I*", sprintf('%08d', $aBlock['version']));
$previousblockhash = pack("H*", swapEndian($aBlock['previousblockhash']));
$merkleroot = pack("H*", swapEndian($aBlock['merkleroot']) );
$time = pack("I*", $aBlock['time']);
$bits = pack("H*", swapEndian($aBlock['bits']));
$nonce = pack("I*", $aBlock['nonce']);
$header_bin = $version . $previousblockhash . $merkleroot . $time . $bits . $nonce;
$header_hex = implode(unpack("H*", $header_bin));
// Stratum supported blockhash solution entry
$stmt = $this->mysqli->prepare("SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, username as worker, id FROM $this->table WHERE solution = ? LIMIT 1");
if ($this->checkStmt($stmt) && $stmt->bind_param('s', $aBlock['hash']) && $stmt->execute() && $result = $stmt->get_result()) {
$this->oUpstream = $result->fetch_object();
$this->share_type = 'stratum_blockhash';
if (!empty($this->oUpstream->account) && !empty($this->oUpstream->worker) && is_int($this->oUpstream->id))
return true;
}
// Stratum scrypt hash check
$scrypt_hash = swapEndian(bin2hex(Scrypt::calc($header_bin, $header_bin, 1024, 1, 1, 32)));
$stmt = $this->mysqli->prepare("SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, username as worker, id FROM $this->table WHERE solution = ? LIMIT 1");
if ($this->checkStmt($stmt) && $stmt->bind_param('s', $scrypt_hash) && $stmt->execute() && $result = $stmt->get_result()) {
$this->oUpstream = $result->fetch_object();
$this->share_type = 'stratum_solution';
if (!empty($this->oUpstream->account) && !empty($this->oUpstream->worker) && is_int($this->oUpstream->id))
return true;
}
// Failed to fetch via startum solution, try pushpoold
// Fallback to pushpoold solution type
$ppheader = sprintf('%08d', $aBlock['version']) . word_reverse($aBlock['previousblockhash']) . word_reverse($aBlock['merkleroot']) . dechex($aBlock['time']) . $aBlock['bits'] . dechex($aBlock['nonce']);
$stmt = $this->mysqli->prepare("SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, username as worker, id FROM $this->table WHERE solution LIKE CONCAT(?, '%') LIMIT 1");
if ($this->checkStmt($stmt) && $stmt->bind_param('s', $ppheader) && $stmt->execute() && $result = $stmt->get_result()) {
$this->oUpstream = $result->fetch_object();
$this->share_type = 'pp_solution';
if (!empty($this->oUpstream->account) && !empty($this->oUpstream->worker) && is_int($this->oUpstream->id))
return true;
}
// Still no match, try upstream result with timerange
$stmt = $this->mysqli->prepare("
SELECT
SUBSTRING_INDEX( `username` , '.', 1 ) AS account, username as worker, id
FROM $this->table
WHERE upstream_result = 'Y'
AND id > ?
AND UNIX_TIMESTAMP(time) >= ?
AND UNIX_TIMESTAMP(time) <= ( ? + 60 )
ORDER BY id ASC LIMIT 1");
if ($this->checkStmt($stmt) && $stmt->bind_param('iii', $last, $aBlock['time'], $aBlock['time']) && $stmt->execute() && $result = $stmt->get_result()) {
$this->oUpstream = $result->fetch_object();
$this->share_type = 'upstream_share';
if (!empty($this->oUpstream->account) && !empty($this->oUpstream->worker) && is_int($this->oUpstream->id))
return true;
}
// We failed again, now we take ANY result matching the timestamp
$stmt = $this->mysqli->prepare("
SELECT
SUBSTRING_INDEX( `username` , '.', 1 ) AS account, username as worker, id
FROM $this->table
WHERE our_result = 'Y'
AND id > ?
AND UNIX_TIMESTAMP(time) >= ?
ORDER BY id ASC LIMIT 1");
if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $last, $aBlock['time']) && $stmt->execute() && $result = $stmt->get_result()) {
$this->oUpstream = $result->fetch_object();
$this->share_type = 'any_share';
if (!empty($this->oUpstream->account) && !empty($this->oUpstream->worker) && is_int($this->oUpstream->id))
return true;
}
$this->setErrorMessage($this->getErrorMsg('E0052', $aBlock['height']));
return false;
}
/**
* Fetch the lowest needed share ID from shares
**/
function getMinimumShareId($iCount, $current_upstream) {
// We don't use baseline here to be more accurate
$iCount = $iCount * pow(2, ($this->config['difficulty'] - 16));
$stmt = $this->mysqli->prepare("
SELECT MIN(b.id) AS id FROM
(
SELECT id, @total := @total + IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty) AS total
FROM $this->table, (SELECT @total := 0) AS a
WHERE our_result = 'Y'
AND id <= ? AND @total < ?
ORDER BY id DESC
) AS b
WHERE total <= ?");
if ($this->checkStmt($stmt) && $stmt->bind_param('iii', $current_upstream, $iCount, $iCount) && $stmt->execute() && $result = $stmt->get_result())
return $result->fetch_object()->id;
return $this->sqlError();
}
/**
* Fetch the lowest needed share ID from archive
**/
function getMinArchiveShareId($iCount) {
// We don't use baseline here to be more accurate
$iCount = $iCount * pow(2, ($this->config['difficulty'] - 16));
$stmt = $this->mysqli->prepare("
SELECT MIN(b.share_id) AS share_id FROM
(
SELECT share_id, @total := @total + IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty) AS total
FROM $this->tableArchive, (SELECT @total := 0) AS a
WHERE our_result = 'Y'
AND @total < ?
ORDER BY share_id DESC
) AS b
WHERE total <= ?
");
if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $iCount, $iCount) && $stmt->execute() && $result = $stmt->get_result())
return $result->fetch_object()->share_id;
return $this->sqlError();
}
}
$share = new Share();
$share->setDebug($debug);
$share->setMysql($mysqli);
$share->setConfig($config);
$share->setUser($user);
$share->setBlock($block);
$share->setErrorCodes($aErrorCodes);