Skip to content

Instantly share code, notes, and snippets.

@YihaoPeng
Created September 13, 2018 09:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save YihaoPeng/b828532e89db10bc5ecef2edc3c934c8 to your computer and use it in GitHub Desktop.
Save YihaoPeng/b828532e89db10bc5ecef2edc3c934c8 to your computer and use it in GitHub Desktop.
orphaned-block-monitor.php
<?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