From ac5c0fce9513f48115e7a0b812668688a63b501a Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 24 Oct 2013 12:04:00 +0200 Subject: [PATCH 1/2] [IMPROVED] Out of Order Share detection * [ADDED] Allow findblocks to detect and fix out of order shares * [IMPROVED] Share class extends Base class for common functions * [IMPROVED] Added more debugging and logging output to cronjob * [ADDED] Added various new methods to blocks, share, base classes For an overview of errors thrown in crons, please check: https://github.com/TheSerapher/php-mpos/wiki/Error-Codes Fixes #759 once merged. --- cronjobs/findblock.php | 98 ++++++++++++++++++-------- public/include/classes/base.class.php | 7 ++ public/include/classes/block.class.php | 24 +++++++ public/include/classes/share.class.php | 73 +++++++++++-------- 4 files changed, 142 insertions(+), 60 deletions(-) diff --git a/cronjobs/findblock.php b/cronjobs/findblock.php index 114d117b..50d03a61 100755 --- a/cronjobs/findblock.php +++ b/cronjobs/findblock.php @@ -51,17 +51,17 @@ if (empty($aTransactions['transactions'])) { // Let us add those blocks as unaccounted foreach ($aTransactions['transactions'] as $iIndex => $aData) { if ( $aData['category'] == 'generate' || $aData['category'] == 'immature' ) { - $aBlockInfo = $bitcoin->query('getblock', $aData['blockhash']); + $aBlockRPCInfo = $bitcoin->query('getblock', $aData['blockhash']); $config['reward_type'] == 'block' ? $aData['amount'] = $aData['amount'] : $aData['amount'] = $config['reward']; - $aData['height'] = $aBlockInfo['height']; - $aData['difficulty'] = $aBlockInfo['difficulty']; + $aData['height'] = $aBlockRPCInfo['height']; + $aData['difficulty'] = $aBlockRPCInfo['difficulty']; $log->logInfo(substr($aData['blockhash'], 0, 15) . "...\t" . $aData['height'] . "\t" . $aData['amount'] . "\t" . $aData['confirmations'] . "\t\t" . $aData['difficulty'] . "\t" . strftime("%Y-%m-%d %H:%M:%S", $aData['time'])); - if ( ! empty($aBlockInfo['flags']) && preg_match('/proof-of-stake/', $aBlockInfo['flags']) ) { + if ( ! empty($aBlockRPCInfo['flags']) && preg_match('/proof-of-stake/', $aBlockRPCInfo['flags']) ) { $log->logInfo("Block above with height " . $aData['height'] . " not added to database, proof-of-stake block!"); continue; } @@ -78,38 +78,65 @@ if (empty($aAllBlocks)) { $log->logDebug('No new blocks without share_id found in database'); } else { // Loop through our unaccounted blocks - $log->logInfo("Block ID\t\tHeight\tAmount\tShare ID\tShares\tFinder\tType"); + $log->logInfo("Block ID\tHeight\t\tAmount\tShare ID\tShares\tFinder\t\tType"); foreach ($aAllBlocks as $iIndex => $aBlock) { if (empty($aBlock['share_id'])) { - // Fetch this blocks upstream ID - $aBlockInfo = $bitcoin->query('getblock', $aBlock['blockhash']); - if ($share->setUpstream($aBlockInfo, $block->getLastUpstreamId())) { - $iCurrentUpstreamId = $share->getUpstreamId(); - $iAccountId = $user->getUserId($share->getUpstreamFinder()); - } else { - $log->logFatal('Unable to fetch blocks upstream share, aborted:' . $share->getError()); - $monitoring->setStatus($cron_name . "_active", "yesno", 0); - $monitoring->setStatus($cron_name . "_message", "message", "Unable to fetch blocks " . $aBlock['height'] . " upstream share: " . $share->getError()); - $monitoring->setStatus($cron_name . "_status", "okerror", 1); - exit; - } - // Fetch share information - if (!$iPreviousShareId = $block->getLastShareId()) { + $iPreviousShareId = $block->getLastShareId(); + if ( !$iPreviousShareId && $block->getBlockCount() > 1) { $iPreviousShareId = 0; - $log->logInfo('Unable to find highest share ID found so far, if this is your first block, this is normal.'); + // $log->logError('Unable to find highest share ID found so far, assuming share ID 0 as previous found upstream share.'); } - $iRoundShares = $share->getRoundShares($iPreviousShareId, $iCurrentUpstreamId); - // Store new information - if (!$block->setShareId($aBlock['id'], $iCurrentUpstreamId)) - $log->logError('Failed to update share ID in database for block ' . $aBlock['height']); - if (!$block->setFinder($aBlock['id'], $iAccountId)) - $log->logError('Failed to update finder account ID in database for block ' . $aBlock['height']); - if (!$block->setShares($aBlock['id'], $iRoundShares)) - $log->logError('Failed to update share count in database for block ' . $aBlock['height']); - if ($config['block_bonus'] > 0 && !$transaction->addTransaction($iAccountId, $config['block_bonus'], 'Bonus', $aBlock['id'])) { - $log->logError('Failed to create Bonus transaction in database for user ' . $user->getUserName($iAccountId) . ' for block ' . $aBlock['height']); + // Fetch this blocks upstream ID + $aBlockRPCInfo = $bitcoin->query('getblock', $aBlock['blockhash']); + if ($share->findUpstreamShare($aBlockRPCInfo, $iPreviousShareId)) { + $iCurrentUpstreamId = $share->getUpstreamShareId(); + // Out of order share detection + if ($iCurrentUpstreamId < $iPreviousShareId) { + // Fetch our offending block + $aBlockError = $block->getBlockByShareId($iPreviousShareId); + $log->logError('E0001: The block with height ' . $aBlock['height'] . ' found share ' . $iCurrentUpstreamId . ' which is < than ' . $iPreviousShareId . ' of block ' . $aBlockError['height'] . '.'); + if ( !$aShareError = $share->getShareById($aBlockError['share_id']) || !$aShareCurrent = $share->getShareById($iCurrentUpstreamId)) { + // We were not able to fetch all shares that were causing this detection to trigger, bail out + $log->logFatal('E0002: Failed to fetch both offending shares ' . $iCurrentUpstreamId . ' and ' . $iPreviousShareId . '. Block height: ' . $aBlock['height']); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "E0002: Upstream shares not found"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); + } + // Shares seem to be out of order, so lets change them + if ( !$share->updateShareById($iCurrentUpstreamId, $aShareError) || !$share->updateShareById($iPreviousShareId, $aShareCurrent)) { + // We couldn't update one of the shares! That might mean they have been deleted already + $log->logFatal('E0003: Failed to change shares order!'); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "E0003: Failed share update"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); + } + // Reset our offending block so the next run re-checks the shares + if (!$block->setShareId($aBlockError['id'], NULL) && !$block->setFinder($aBlockError['id'], NULL) || !$block->setShares($aBlockError['id'], NULL)) { + $log->logFatal('E0004: Failed to reset previous block: ' . $aBlockError['height']); + $log->logError('Failed to reset block in database: ' . $aBlockError['height']); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "E0004: Failed to reset block"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); + } + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Out of Order Share detected, autofixed"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(0); + } else { + $iRoundShares = $share->getRoundShares($iPreviousShareId, $iCurrentUpstreamId); + $iAccountId = $user->getUserId($share->getUpstreamFinder()); + } + } else { + $log->logFatal('E0005: Unable to fetch blocks upstream share, aborted:' . $share->getError()); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to fetch blocks " . $aBlock['height'] . " upstream share: " . $share->getError()); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); } $log->logInfo( @@ -122,6 +149,17 @@ if (empty($aAllBlocks)) { . $share->share_type ); + // Store new information + if (!$block->setShareId($aBlock['id'], $iCurrentUpstreamId)) + $log->logError('Failed to update share ID in database for block ' . $aBlock['height']); + if (!$block->setFinder($aBlock['id'], $iAccountId)) + $log->logError('Failed to update finder account ID in database for block ' . $aBlock['height']); + if (!$block->setShares($aBlock['id'], $iRoundShares)) + $log->logError('Failed to update share count in database for block ' . $aBlock['height']); + if ($config['block_bonus'] > 0 && !$transaction->addTransaction($iAccountId, $config['block_bonus'], 'Bonus', $aBlock['id'])) { + $log->logError('Failed to create Bonus transaction in database for user ' . $user->getUserName($iAccountId) . ' for block ' . $aBlock['height']); + } + if ($setting->getValue('disable_notifications') != 1) { // Notify users $aAccounts = $notification->getNotificationAccountIdByType('new_block'); diff --git a/public/include/classes/base.class.php b/public/include/classes/base.class.php index 2aae51ce..fe548cbc 100644 --- a/public/include/classes/base.class.php +++ b/public/include/classes/base.class.php @@ -50,6 +50,13 @@ class Base { return $this->sError; } + protected function getAllAssoc($value, $field='id', $type='i') { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE $field = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param($type, $value) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_assoc(); + return false; + } /** * Get a single row from the table * @param value string Value to search for diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index 027d06c5..bdb30cb1 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -56,6 +56,30 @@ class Block { return false; } + /** + * Get a specific block, by share_id + * @param share_id int Blocks share_id + * @return data array Block information from DB + **/ + public function getBlockByShareId($share_id) { + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE share_id = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $share_id) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_assoc(); + return false; + } + + /** + * Get a specific block, by id + * @param share_id int Blocks share_id + * @return data array Block information from DB + **/ + public function getBlockById($id) { + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE id = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $id) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_assoc(); + return false; + } + /** * Get our last, highest share ID inserted for a block * @param none diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index 705c41e1..be126789 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -4,9 +4,8 @@ if (!defined('SECURITY')) die('Hacking attempt'); -class Share { - private $sError = ''; - private $table = 'shares'; +class Share Extends Base { + protected $table = 'shares'; private $tableArchive = 'shares_archive'; private $oUpstream; private $iLastUpstreamId; @@ -22,14 +21,6 @@ class Share { $this->debug->append("Instantiated Share class", 2); } - // get and set methods - private function setErrorMessage($msg) { - $this->sError = $msg; - } - public function getError() { - return $this->sError; - } - /** * Fetch archive tables name for this class * @param none @@ -38,13 +29,43 @@ class Share { public function getArchiveTableName() { return $this->tableArchive; } + /** - * Fetch normal table name for this class - * @param none - * @return data string Table name + * Fetch a single share by ID + * @param id int Share ID + * @return array Share data **/ - public function getTableName() { - return $this->table; + 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 false; } /** @@ -229,7 +250,7 @@ class Share { public function getUpstreamFinder() { return @$this->oUpstream->account; } - public function getUpstreamId() { + public function getUpstreamShareId() { return @$this->oUpstream->id; } /** @@ -240,7 +261,7 @@ class Share { * @param last int Skips all shares up to last to find new share * @return bool **/ - public function setUpstream($aBlock, $last=0) { + 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'])); @@ -358,18 +379,10 @@ class Share { return $result->fetch_object()->share_id; return false; } - - /** - * Helper function - **/ - private function checkStmt($bState) { - if ($bState ===! true) { - $this->debug->append("Failed to prepare statement: " . $this->mysqli->error); - $this->setErrorMessage('Internal application Error'); - return false; - } - return true; - } } $share = new Share($debug, $mysqli, $user, $block, $config); +$share->setMysql($mysqli); +$share->setConfig($config); +$share->setUser($user); +$share->setBlock($block); From 412fbe3f5b1dac87d179f15c17c2d162d392e303 Mon Sep 17 00:00:00 2001 From: Sebastian Grewe Date: Thu, 24 Oct 2013 12:32:58 +0200 Subject: [PATCH 2/2] [FIX] Proper getTableName in Base Class --- public/include/classes/base.class.php | 4 ++++ public/include/classes/transaction.class.php | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/include/classes/base.class.php b/public/include/classes/base.class.php index fe548cbc..1f059c37 100644 --- a/public/include/classes/base.class.php +++ b/public/include/classes/base.class.php @@ -8,8 +8,12 @@ if (!defined('SECURITY')) // some cross-class functions. class Base { private $sError = ''; + protected $table = ''; private $values = array(), $types = ''; + public function getTableName() { + return $this->table; + } public function setDebug($debug) { $this->debug = $debug; } diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 53d111e3..3933079f 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -5,7 +5,8 @@ if (!defined('SECURITY')) die('Hacking attempt'); class Transaction extends Base { - private $sError = '', $table = 'transactions'; + private $sError = ''; + protected $table = 'transactions'; public $num_rows = 0, $insert_id = 0; /**