Created
September 13, 2018 09:26
-
-
Save YihaoPeng/b828532e89db10bc5ecef2edc3c934c8 to your computer and use it in GitHub Desktop.
orphaned-block-monitor.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* 孤块监控 | |
* orphaned-block-monitor.php | |
* @author SwimmingTiger <hu60.cn@gmail.com> | |
*/ | |
// 起始区块序号 | |
define('BEGIN_BLOCK_OFFSET', 0); | |
// 数据库信息 | |
define('DB_HOST', 'localhost'); | |
define('DB_NAME', 'bpool_local_db'); | |
define('DB_USER', 'root'); | |
define('DB_PASSWD', 'root'); | |
// 区块链浏览器信息 | |
define('BLOCK_INFO_API_URL', 'https://chain.api.btc.com/v3/block/'); | |
define('BLOCK_BROWSER_URL', 'https://btc.com/'); | |
//------------------------------------------ | |
$offset = BEGIN_BLOCK_OFFSET; | |
$size = 26; | |
$orphanedBlockNum = getOrphanedBlockNumFromDB(BEGIN_BLOCK_OFFSET); | |
// 是否为旧区块,用来控制延时(从浏览器读取新区块的延时要长一些) | |
$isOldBlock = true; | |
while (true) { | |
// 顺带检查前6个区块,防止统计出错 | |
$blocks = getBlockFromDB(max(0, $offset-6), $size); | |
$blockNum = count($blocks); | |
// 只有前6个区块,说明没有新区块 | |
if (($offset <= 6 && empty($blocks)) || (count($blocks) < 6)) { | |
logLine("无更多区块,60秒后再次检查"); | |
$isOldBlock = false; | |
sleep(60); | |
continue; | |
} | |
$blockHeights = []; | |
foreach ($blocks as $block) { | |
$blockHeights[] = $block->height; | |
} | |
// 将延时添加在这里,给区块留下足够的时间传播到区块浏览器,减少误报 | |
sleep($isOldBlock ? 5 : 60); | |
$blockInfos = getBlockInfoFromBrowser($blockHeights, $blockInfosURL); | |
// 检查区块链浏览器返回的数据 | |
if (!is_object($blockInfos) || !isset($blockInfos->err_no)) { | |
$msg = "区块浏览器返回数据格式错误,30秒后重试\n" . json_encode($blockInfos); | |
logLine($msg); | |
sleep(30); | |
continue; | |
} | |
if ($blockInfos->err_no !== 0 || $blockInfos->err_msg !== null) { | |
$msg = "区块浏览器返回一个错误,30秒后重试\n$blockInfosURL\n" . json_encode($blockInfos); | |
logLine($msg); | |
sleep(30); | |
continue; | |
} | |
// 在只有一条结果时,区块链浏览器尴尬的不返回数组 | |
if ($blockNum == 1) { | |
$blockInfos->data = [$blockInfos->data]; | |
} | |
if (!is_array($blockInfos->data) || count($blockInfos->data) !== $blockNum) { | |
$msg = "区块浏览器返回的数据数量与预期不符,30秒后重试\n$blockInfosURL\n" . json_encode($blockInfos); | |
logLine($msg); | |
sleep(30); | |
continue; | |
} | |
// 检查是否为孤块 | |
for ($i=0; $i<$blockNum; $i++) { | |
$block = $blocks[$i]; | |
$blockInfo = $blockInfos->data[$i]; | |
$blockUrl = BLOCK_BROWSER_URL . $block->hash; | |
if ($blockInfo === NULL) { | |
$msg = "区块 $block->height ($block->created_at) 不在区块链中\n$blockUrl\n"; | |
$msg .= "未发现竞争区块 ".BLOCK_BROWSER_URL.$block->height."\n"; | |
$msg .= "60秒后将再次检查"; | |
logLine($msg); | |
// 重新开始最外层循环 | |
continue 2; | |
} | |
if ($block->height != $blockInfo->height) { | |
$msg = "区块 $block->height ($block->created_at) 的块高与浏览器返回的({$blockInfo->height} )不一致,请检查代码逻辑\n$blockUrl"; | |
logLine($msg); | |
// 重新开始最外层循环 | |
continue 2; | |
} | |
if ($block->hash !== $blockInfo->hash || $blockInfo->is_orphan === true) { | |
$msg = "区块 $block->height ($block->created_at) 为孤块\n$blockUrl\n"; | |
$msg .= "竞争区块 ".BLOCK_BROWSER_URL.$blockInfo->hash; | |
logLine($msg); | |
$orphanedBlockNum++; | |
if (!$block->is_orphaned) { | |
UpdateIsOrphanedToDB($block->id, true); | |
} | |
// 继续检查下一个块 | |
continue; | |
} | |
// 区块不应为孤块 | |
if ($block->is_orphaned) { | |
$msg = "区块 $block->height ($block->created_at) 被误标为孤块,已更正\n$blockUrl"; | |
logLine($msg); | |
UpdateIsOrphanedToDB($block->id, false); | |
continue; | |
} | |
$msg = "区块 $block->height ($block->created_at) 状态正常\n$blockUrl"; | |
logLine($msg); | |
continue; | |
} | |
$scannedBlockNum = $offset + $blockNum; | |
$lastBlockHeight = $blockHeights[$blockNum - 1]; | |
logLine("已扫描区块数:{$scannedBlockNum},孤块数:{$orphanedBlockNum},末块高度:{$lastBlockHeight},5秒后继续"); | |
$offset += $blockNum; | |
} | |
// 连接数据库 | |
function dbConn() { | |
static $conn = null; | |
if ($conn === null) { | |
$conn = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME, DB_USER, DB_PASSWD); | |
$conn->exec('SET NAMES utf8'); | |
} | |
return $conn; | |
} | |
// 从数据库获取区块 | |
function getBlockFromDB($offset, $size) { | |
$offset = (int)$offset; | |
$size = (int)$size; | |
$sql = "SELECT * FROM found_blocks ORDER BY id ASC LIMIT $offset, $size"; | |
$db = dbConn(); | |
$rs = $db->query($sql); | |
return $rs->fetchAll(PDO::FETCH_OBJ); | |
} | |
// 从数据库获取指定位置之前的孤块数 | |
function getOrphanedBlockNumFromDB($offset) { | |
$offset = (int)$offset; | |
$sql = "SELECT count(*) FROM found_blocks WHERE is_orphaned = 1 AND id < (SELECT id FROM found_blocks ORDER BY id ASC LIMIT $offset, 1)"; | |
$db = dbConn(); | |
$rs = $db->query($sql); | |
return $rs->fetch(PDO::FETCH_NUM)[0]; | |
} | |
// 更新孤块状态 | |
function UpdateIsOrphanedToDB($id, $isOrphaned) { | |
$id = (int)$id; | |
$isOrphaned = (int)$isOrphaned; | |
$sql = "UPDATE found_blocks SET is_orphaned=$isOrphaned WHERE id=$id"; | |
$db = dbConn(); | |
$rs = $db->exec($sql); | |
return $rs; | |
} | |
// 从区块链浏览器获取区块信息 | |
function getBlockInfoFromBrowser($blockHeights, &$requestURL) { | |
if (is_array($blockHeights)) { | |
$blockHeights = implode(',', $blockHeights); | |
} | |
$requestURL = BLOCK_INFO_API_URL . $blockHeights; | |
$json = httpGET($requestURL); | |
$result = json_decode($json); | |
if ($result === NULL) { | |
$result = $json; | |
} | |
return $result; | |
} | |
// HTTP GET 请求 | |
function httpGET($url, $auth = null) { | |
$ch = curl_init(); | |
$options = [ | |
CURLOPT_URL => $url, | |
CURLOPT_USERAGENT => "btc-rpc-test v0.1", | |
CURLOPT_RETURNTRANSFER => 1, | |
CURLOPT_USERPWD => $auth, | |
]; | |
curl_setopt_array($ch, $options); | |
$result = curl_exec($ch); | |
curl_close($ch); | |
return $result; | |
} | |
// HTTP POST 请求 | |
function httpPOST($url, $data, $auth = null, $mime = 'application/json') { | |
$ch = curl_init(); | |
$options = [ | |
CURLOPT_URL => $url, | |
CURLOPT_USERAGENT => "btc-rpc-test v0.1", | |
CURLOPT_RETURNTRANSFER => 1, | |
CURLOPT_USERPWD => $auth, | |
CURLOPT_HTTPHEADER => ['Content-Type: '.$mime], | |
]; | |
curl_setopt_array($ch, $options); | |
curl_setopt($ch, CURLOPT_POSTFIELDS, $data); | |
$result = curl_exec($ch); | |
curl_close($ch); | |
return $result; | |
} | |
// 输出日志 | |
function logLine($msg) { | |
echo date('[Y-m-d H:i:s] '), $msg, "\n"; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment