These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | $defflip = (!cfip()) ? exit(header('HTTP/1.1 401 Unauthorized')) : 1; |
||
3 | |||
4 | class Share Extends Base { |
||
5 | protected $table = 'shares'; |
||
6 | protected $tableArchive = 'shares_archive'; |
||
7 | private $oUpstream; |
||
8 | private $iLastUpstreamId; |
||
9 | // This defines each share |
||
10 | public $rem_host, $username, $our_result, $upstream_result, $reason, $solution, $time, $difficulty; |
||
11 | |||
12 | /** |
||
13 | * Fetch archive tables name for this class |
||
14 | * @param none |
||
15 | * @return data string Table name |
||
16 | **/ |
||
17 | public function getArchiveTableName() { |
||
18 | return $this->tableArchive; |
||
19 | } |
||
20 | |||
21 | /** |
||
22 | * Fetch a single share by ID |
||
23 | * @param id int Share ID |
||
24 | * @return array Share data |
||
25 | **/ |
||
26 | public function getShareById($id) { |
||
27 | return $this->getSingleAssoc($id); |
||
28 | } |
||
29 | |||
30 | /** |
||
31 | * Update an entire shares data |
||
32 | **/ |
||
33 | public function updateShareById($id, $data) { |
||
34 | $this->debug->append("STA " . __METHOD__, 4); |
||
35 | $sql = "UPDATE $this->table SET"; |
||
36 | $start = true; |
||
37 | // Remove ID column |
||
38 | unset($data['id']); |
||
39 | foreach ($data as $column => $value) { |
||
40 | $start == true ? $sql .= " $column = ? " : $sql .= ", $column = ?"; |
||
41 | $start = false; |
||
42 | switch($column) { |
||
43 | case 'difficulty': |
||
44 | $this->addParam('d', $value); |
||
45 | break; |
||
46 | default: |
||
47 | $this->addParam('s', $value); |
||
48 | break; |
||
49 | } |
||
50 | } |
||
51 | $sql .= " WHERE id = ? LIMIT 1"; |
||
52 | $this->addParam('i', $id); |
||
53 | $stmt = $this->mysqli->prepare($sql); |
||
54 | View Code Duplication | if ($this->checkStmt($stmt) && call_user_func_array( array($stmt, 'bind_param'), $this->getParam()) && $stmt->execute()) |
|
55 | return true; |
||
56 | return $this->sqlError(); |
||
57 | } |
||
58 | |||
59 | /** |
||
60 | * Get last inserted Share ID from Database |
||
61 | * Used for PPS calculations without moving to archive |
||
62 | **/ |
||
63 | public function getLastInsertedShareId() { |
||
64 | $stmt = $this->mysqli->prepare("SELECT MAX(id) AS id FROM $this->table"); |
||
65 | View Code Duplication | if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) |
|
0 ignored issues
–
show
|
|||
66 | return $result->fetch_object()->id; |
||
67 | return $this->sqlError(); |
||
68 | } |
||
69 | |||
70 | /** |
||
71 | * Get all valid shares for this round |
||
72 | * @param previous_upstream int Previous found share accepted by upstream to limit results |
||
73 | * @param current_upstream int Current upstream accepted share |
||
74 | * @return data int Total amount of counted shares |
||
75 | **/ |
||
76 | public function getRoundShares($previous_upstream=0, $current_upstream) { |
||
77 | $stmt = $this->mysqli->prepare("SELECT |
||
78 | IFNULL(SUM(IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty)), 0) AS total |
||
79 | FROM $this->table AS s |
||
80 | LEFT JOIN " . $this->user->getTableName() . " AS a |
||
81 | ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) |
||
82 | WHERE s.id > ? AND s.id <= ? AND s.our_result = 'Y' AND a.is_locked != 2 |
||
83 | "); |
||
84 | if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute() && $result = $stmt->get_result()) |
||
85 | return $result->fetch_object()->total; |
||
86 | return $this->sqlError(); |
||
87 | } |
||
88 | |||
89 | /** |
||
90 | * Fetch all shares grouped by accounts to count share per account |
||
91 | * @param previous_upstream int Previous found share accepted by upstream to limit results |
||
92 | * @param current_upstream int Current upstream accepted share |
||
93 | * @param limit int Limit to this amount of shares for PPLNS |
||
94 | * @return data array username, valid and invalid shares from account |
||
95 | **/ |
||
96 | public function getSharesForAccounts($previous_upstream=0, $current_upstream) { |
||
97 | $stmt = $this->mysqli->prepare(" |
||
98 | SELECT |
||
99 | a.id, |
||
100 | SUBSTRING_INDEX( s.username , '.', 1 ) as username, |
||
101 | a.no_fees AS no_fees, |
||
102 | IFNULL(SUM(IF(our_result='Y', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) AS valid, |
||
103 | IFNULL(SUM(IF(our_result='N', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) AS invalid |
||
104 | FROM $this->table AS s |
||
105 | LEFT JOIN " . $this->user->getTableName() . " AS a |
||
106 | ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) |
||
107 | WHERE s.id > ? AND s.id <= ? AND a.is_locked != 2 |
||
108 | GROUP BY username DESC |
||
109 | "); |
||
110 | if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute() && $result = $stmt->get_result()) |
||
111 | return $result->fetch_all(MYSQLI_ASSOC); |
||
112 | return $this->sqlError(); |
||
113 | } |
||
114 | |||
115 | /** |
||
116 | * Fetch the highest available share ID from archive |
||
117 | **/ |
||
118 | function getMaxArchiveShareId() { |
||
119 | $stmt = $this->mysqli->prepare("SELECT MAX(share_id) AS share_id FROM $this->tableArchive"); |
||
120 | if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) |
||
121 | return $result->fetch_object()->share_id; |
||
122 | return $this->sqlError(); |
||
123 | } |
||
124 | |||
125 | /** |
||
126 | * We need a certain amount of valid archived shares |
||
127 | * param left int Left/lowest share ID |
||
128 | * param right int Right/highest share ID |
||
129 | * return array data Returns an array with usernames as keys for easy access |
||
130 | **/ |
||
131 | function getArchiveShares($iCount) { |
||
132 | $iMaxId = $this->getMaxArchiveShareId(); |
||
133 | $iMinId = $this->getMinArchiveShareId($iCount); |
||
134 | $stmt = $this->mysqli->prepare(" |
||
135 | SELECT |
||
136 | a.id, |
||
137 | SUBSTRING_INDEX( s.username , '.', 1 ) as account, |
||
138 | a.no_fees AS no_fees, |
||
139 | IFNULL(SUM(IF(our_result='Y', IF(s.difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) AS valid, |
||
140 | IFNULL(SUM(IF(our_result='N', IF(s.difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) AS invalid |
||
141 | FROM $this->tableArchive AS s |
||
142 | LEFT JOIN " . $this->user->getTableName() . " AS a |
||
143 | ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) |
||
144 | WHERE s.share_id > ? AND s.share_id <= ? AND a.is_locked != 2 |
||
145 | GROUP BY account DESC"); |
||
146 | if ($this->checkStmt($stmt) && $stmt->bind_param("ii", $iMinId, $iMaxId) && $stmt->execute() && $result = $stmt->get_result()) { |
||
147 | $aData = NULL; |
||
148 | while ($row = $result->fetch_assoc()) { |
||
149 | $aData[strtolower($row['account'])] = $row; |
||
150 | } |
||
151 | if (is_array($aData)) return $aData; |
||
152 | } |
||
153 | return $this->sqlError(); |
||
154 | } |
||
155 | |||
156 | /** |
||
157 | * We keep shares only up to a certain point |
||
158 | * This can be configured by the user. |
||
159 | * @return return bool true or false |
||
160 | **/ |
||
161 | public function purgeArchive() { |
||
162 | // Fallbacks if unset |
||
163 | if (!isset($this->config['archive']['purge'])) $this->config['archive']['purge'] = 5; |
||
164 | |||
165 | $stmt = $this->mysqli->prepare("SELECT CEIL(COUNT(id) / 100 * ?) AS count FROM $this->tableArchive"); |
||
166 | View Code Duplication | if ($this->checkStmt($stmt) && $stmt->bind_param('i', $this->config['archive']['purge']) && $stmt->execute() && $result = $stmt->get_result()) { |
|
167 | $limit = $result->fetch_object()->count; |
||
168 | } else { |
||
169 | return $this->sqlError(); |
||
170 | } |
||
171 | $stmt->close(); |
||
172 | $stmt = $this->mysqli->prepare(" |
||
173 | DELETE FROM $this->tableArchive WHERE time < ( |
||
174 | SELECT MIN(time) FROM ( |
||
175 | SELECT MIN(time) AS time |
||
176 | FROM $this->tableArchive |
||
177 | WHERE block_id = ( |
||
178 | SELECT MIN(id) AS minid FROM ( |
||
179 | SELECT id FROM " . $this->block->getTableName() . " ORDER BY height DESC LIMIT ? |
||
180 | ) AS minheight |
||
181 | ) UNION SELECT DATE_SUB(now(), INTERVAL ? MINUTE) AS time |
||
182 | ) AS mintime |
||
183 | ) LIMIT $limit |
||
184 | "); |
||
185 | if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $this->config['archive']['maxrounds'], $this->config['archive']['maxage']) && $stmt->execute()) |
||
186 | return $stmt->affected_rows; |
||
187 | return $this->sqlError(); |
||
188 | } |
||
189 | |||
190 | /** |
||
191 | * Move accounted shares to archive table, this step is optional |
||
192 | * @param previous_upstream int Previous found share accepted by upstream to limit results |
||
193 | * @param current_upstream int Current upstream accepted share |
||
194 | * @param block_id int Block ID to assign shares to a specific block |
||
195 | * @return bool |
||
196 | **/ |
||
197 | public function moveArchive($current_upstream, $block_id, $previous_upstream=0) { |
||
198 | if ($this->config['payout_system'] != 'pplns') { |
||
199 | // We don't need archived shares that much, so only archive as much as configured |
||
200 | $sql = " |
||
201 | INSERT INTO $this->tableArchive (share_id, username, our_result, upstream_result, block_id, time, difficulty) |
||
202 | SELECT id, username, our_result, upstream_result, ?, time, IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty) AS difficulty |
||
203 | FROM $this->table |
||
204 | WHERE id > ? AND id <= ? |
||
205 | AND time >= DATE_SUB(now(), INTERVAL " . $this->config['archive']['maxage'] . " MINUTE)"; |
||
206 | } else { |
||
207 | // PPLNS needs archived shares for later rounds, so we have to copy them all |
||
208 | $sql = " |
||
209 | INSERT INTO $this->tableArchive (share_id, username, our_result, upstream_result, block_id, time, difficulty) |
||
210 | SELECT id, username, our_result, upstream_result, ?, time, IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty) AS difficulty |
||
211 | FROM $this->table |
||
212 | WHERE id > ? AND id <= ?"; |
||
213 | } |
||
214 | $archive_stmt = $this->mysqli->prepare($sql); |
||
215 | if ($this->checkStmt($archive_stmt) && $archive_stmt->bind_param('iii', $block_id, $previous_upstream, $current_upstream) && $archive_stmt->execute()) |
||
216 | return true; |
||
217 | return $this->sqlError(); |
||
218 | } |
||
219 | |||
220 | /** |
||
221 | * Delete accounted shares from shares table |
||
222 | * @param current_upstream int Current highest upstream ID |
||
223 | * @param previous_upstream int Previous upstream ID |
||
224 | * @return bool true or false |
||
225 | **/ |
||
226 | public function deleteAccountedShares($current_upstream, $previous_upstream=0) { |
||
227 | // Fallbacks if unset |
||
228 | if (!isset($this->config['purge']['shares'])) $this->config['purge']['shares'] = 25000; |
||
229 | if (!isset($this->config['purge']['sleep'])) $this->config['purge']['sleep'] = 1; |
||
230 | |||
231 | $affected = 1; |
||
232 | while ($affected > 0) { |
||
233 | // Sleep first to allow any IO to cleanup |
||
234 | sleep($this->config['purge']['sleep']); |
||
235 | $stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE id > ? AND id <= ? ORDER BY id LIMIT " . $this->config['purge']['shares']); |
||
236 | $start = microtime(true); |
||
237 | View Code Duplication | if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute()) { |
|
238 | $affected = $stmt->affected_rows; |
||
239 | } else { |
||
240 | return $this->sqlError(); |
||
241 | } |
||
242 | } |
||
243 | return true; |
||
244 | } |
||
245 | |||
246 | /** |
||
247 | * Set/get last found share accepted by upstream: id and accounts |
||
248 | **/ |
||
249 | public function setLastUpstreamId() { |
||
250 | $this->iLastUpstreamId = @$this->oUpstream->id ? $this->oUpstream->id : 0; |
||
251 | } |
||
252 | public function getLastUpstreamId() { |
||
253 | return @$this->iLastUpstreamId ? @$this->iLastUpstreamId : 0; |
||
254 | } |
||
255 | public function getUpstreamFinder() { |
||
256 | return @$this->oUpstream->account; |
||
257 | } |
||
258 | public function getUpstreamWorker() { |
||
259 | return @$this->oUpstream->worker; |
||
260 | } |
||
261 | public function getUpstreamShareId() { |
||
262 | return @$this->oUpstream->id; |
||
263 | } |
||
264 | /** |
||
265 | * Find upstream accepted share that should be valid for a specific block |
||
266 | * Assumptions: |
||
267 | * * Shares are matching blocks in ASC order |
||
268 | * * We can skip all upstream shares of previously found ones used in a block |
||
269 | * @param last int Skips all shares up to last to find new share |
||
270 | * @return bool |
||
271 | **/ |
||
272 | public function findUpstreamShare($aBlock, $last=0) { |
||
273 | // Many use stratum, so we create our stratum check first |
||
274 | $version = pack("I*", sprintf('%08d', $aBlock['version'])); |
||
275 | $previousblockhash = pack("H*", swapEndian($aBlock['previousblockhash'])); |
||
276 | $merkleroot = pack("H*", swapEndian($aBlock['merkleroot']) ); |
||
277 | $time = pack("I*", $aBlock['time']); |
||
278 | $bits = pack("H*", swapEndian($aBlock['bits'])); |
||
279 | $nonce = pack("I*", $aBlock['nonce']); |
||
280 | $header_bin = $version . $previousblockhash . $merkleroot . $time . $bits . $nonce; |
||
281 | $header_hex = implode(unpack("H*", $header_bin)); |
||
282 | |||
283 | // Stratum supported blockhash solution entry |
||
284 | $stmt = $this->mysqli->prepare("SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, username as worker, id FROM $this->table WHERE solution = ? LIMIT 1"); |
||
285 | View Code Duplication | if ($this->checkStmt($stmt) && $stmt->bind_param('s', $aBlock['hash']) && $stmt->execute() && $result = $stmt->get_result()) { |
|
286 | $this->oUpstream = $result->fetch_object(); |
||
287 | $this->share_type = 'stratum_blockhash'; |
||
288 | if (!empty($this->oUpstream->account) && !empty($this->oUpstream->worker) && is_int($this->oUpstream->id)) |
||
289 | return true; |
||
290 | } |
||
291 | |||
292 | // Stratum scrypt hash check |
||
293 | $scrypt_hash = swapEndian(bin2hex(Scrypt::calc($header_bin, $header_bin, 1024, 1, 1, 32))); |
||
294 | $stmt = $this->mysqli->prepare("SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, username as worker, id FROM $this->table WHERE solution = ? LIMIT 1"); |
||
295 | View Code Duplication | if ($this->checkStmt($stmt) && $stmt->bind_param('s', $scrypt_hash) && $stmt->execute() && $result = $stmt->get_result()) { |
|
296 | $this->oUpstream = $result->fetch_object(); |
||
297 | $this->share_type = 'stratum_solution'; |
||
298 | if (!empty($this->oUpstream->account) && !empty($this->oUpstream->worker) && is_int($this->oUpstream->id)) |
||
299 | return true; |
||
300 | } |
||
301 | |||
302 | // Failed to fetch via startum solution, try pushpoold |
||
303 | // Fallback to pushpoold solution type |
||
304 | $ppheader = sprintf('%08d', $aBlock['version']) . word_reverse($aBlock['previousblockhash']) . word_reverse($aBlock['merkleroot']) . dechex($aBlock['time']) . $aBlock['bits'] . dechex($aBlock['nonce']); |
||
305 | $stmt = $this->mysqli->prepare("SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, username as worker, id FROM $this->table WHERE solution LIKE CONCAT(?, '%') LIMIT 1"); |
||
306 | View Code Duplication | if ($this->checkStmt($stmt) && $stmt->bind_param('s', $ppheader) && $stmt->execute() && $result = $stmt->get_result()) { |
|
307 | $this->oUpstream = $result->fetch_object(); |
||
308 | $this->share_type = 'pp_solution'; |
||
309 | if (!empty($this->oUpstream->account) && !empty($this->oUpstream->worker) && is_int($this->oUpstream->id)) |
||
310 | return true; |
||
311 | } |
||
312 | |||
313 | // Still no match, try upstream result with timerange |
||
314 | $stmt = $this->mysqli->prepare(" |
||
315 | SELECT |
||
316 | SUBSTRING_INDEX( `username` , '.', 1 ) AS account, username as worker, id |
||
317 | FROM $this->table |
||
318 | WHERE upstream_result = 'Y' |
||
319 | AND id > ? |
||
320 | AND UNIX_TIMESTAMP(time) >= ? |
||
321 | AND UNIX_TIMESTAMP(time) <= ( ? + 60 ) |
||
322 | ORDER BY id ASC LIMIT 1"); |
||
323 | View Code Duplication | if ($this->checkStmt($stmt) && $stmt->bind_param('iii', $last, $aBlock['time'], $aBlock['time']) && $stmt->execute() && $result = $stmt->get_result()) { |
|
324 | $this->oUpstream = $result->fetch_object(); |
||
325 | $this->share_type = 'upstream_share'; |
||
326 | if (!empty($this->oUpstream->account) && !empty($this->oUpstream->worker) && is_int($this->oUpstream->id)) |
||
327 | return true; |
||
328 | } |
||
329 | |||
330 | // We failed again, now we take ANY result matching the timestamp |
||
331 | $stmt = $this->mysqli->prepare(" |
||
332 | SELECT |
||
333 | SUBSTRING_INDEX( `username` , '.', 1 ) AS account, username as worker, id |
||
334 | FROM $this->table |
||
335 | WHERE our_result = 'Y' |
||
336 | AND id > ? |
||
337 | AND UNIX_TIMESTAMP(time) >= ? |
||
338 | ORDER BY id ASC LIMIT 1"); |
||
339 | View Code Duplication | if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $last, $aBlock['time']) && $stmt->execute() && $result = $stmt->get_result()) { |
|
340 | $this->oUpstream = $result->fetch_object(); |
||
341 | $this->share_type = 'any_share'; |
||
342 | if (!empty($this->oUpstream->account) && !empty($this->oUpstream->worker) && is_int($this->oUpstream->id)) |
||
343 | return true; |
||
344 | } |
||
345 | $this->setErrorMessage($this->getErrorMsg('E0052', $aBlock['height'])); |
||
346 | return false; |
||
347 | } |
||
348 | |||
349 | /** |
||
350 | * Fetch the lowest needed share ID from shares |
||
351 | **/ |
||
352 | View Code Duplication | function getMinimumShareId($iCount, $current_upstream) { |
|
353 | $stmt = $this->mysqli->prepare(" |
||
354 | SELECT MIN(b.id) AS id FROM |
||
355 | ( |
||
356 | SELECT id, @total := @total + IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty) AS total |
||
357 | FROM $this->table, (SELECT @total := 0) AS a |
||
358 | WHERE our_result = 'Y' |
||
359 | AND id <= ? AND @total < ? |
||
360 | ORDER BY id DESC |
||
361 | ) AS b |
||
362 | WHERE total <= ?"); |
||
363 | if ($this->checkStmt($stmt) && $stmt->bind_param('iii', $current_upstream, $iCount, $iCount) && $stmt->execute() && $result = $stmt->get_result()) |
||
364 | return $result->fetch_object()->id; |
||
365 | return $this->sqlError(); |
||
366 | } |
||
367 | |||
368 | /** |
||
369 | * Fetch the lowest needed share ID from archive |
||
370 | **/ |
||
371 | View Code Duplication | function getMinArchiveShareId($iCount) { |
|
372 | $stmt = $this->mysqli->prepare(" |
||
373 | SELECT MIN(b.share_id) AS share_id FROM |
||
374 | ( |
||
375 | SELECT share_id, @total := @total + IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty) AS total |
||
376 | FROM $this->tableArchive, (SELECT @total := 0) AS a |
||
377 | WHERE our_result = 'Y' |
||
378 | AND @total < ? |
||
379 | ORDER BY share_id DESC |
||
380 | ) AS b |
||
381 | WHERE total <= ? |
||
382 | "); |
||
383 | if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $iCount, $iCount) && $stmt->execute() && $result = $stmt->get_result()) |
||
384 | return $result->fetch_object()->share_id; |
||
385 | return $this->sqlError(); |
||
386 | } |
||
387 | } |
||
388 | |||
389 | $share = new Share(); |
||
390 | $share->setDebug($debug); |
||
391 | $share->setMysql($mysqli); |
||
392 | $share->setConfig($config); |
||
393 | $share->setUser($user); |
||
394 | $share->setBlock($block); |
||
395 | $share->setErrorCodes($aErrorCodes); |
||
396 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.