Merge pull request #187 from cubedro/develop

Added fork check
This commit is contained in:
Marian OANCΞA 2015-06-09 19:49:06 +03:00
commit 01eee61d3b
9 changed files with 323 additions and 75 deletions

2
dist/index.html vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -30,7 +30,7 @@ Collection.prototype.update = function(id, stats, callback)
{ {
this._blockchain.clean(this.getBestBlockFromItems()); this._blockchain.clean(this.getBestBlockFromItems());
var block = this._blockchain.add(stats.block, id); var block = this._blockchain.add(stats.block, id, node.trusted);
if (!block) if (!block)
{ {
@ -61,7 +61,7 @@ Collection.prototype.addBlock = function(id, stats, callback)
{ {
this._blockchain.clean(this.getBestBlockFromItems()); this._blockchain.clean(this.getBestBlockFromItems());
var block = this._blockchain.add(stats, id); var block = this._blockchain.add(stats, id, node.trusted);
if (!block) if (!block)
{ {
@ -121,7 +121,7 @@ Collection.prototype.addHistory = function(id, blocks, callback)
for (var i = 0; i <= blocks.length - 1; i++) for (var i = 0; i <= blocks.length - 1; i++)
{ {
this._blockchain.add(blocks[i], id); this._blockchain.add(blocks[i], id, node.trusted, true);
}; };
this.getCharts(); this.getCharts();
@ -262,9 +262,9 @@ Collection.prototype.getHistory = function()
Collection.prototype.getBestBlockFromItems = function() Collection.prototype.getBestBlockFromItems = function()
{ {
return _.result(_.max(this._items, function(item) { return Math.max(this._blockchain.bestBlockNumber(), _.result(_.max(this._items, function(item) {
return item.stats.block.number; return ( !item.trusted ? 0 : item.stats.block.number );
}), 'stats.block.number', 0); }), 'stats.block.number', 0));
} }
Collection.prototype.canNodeUpdate = function(id) Collection.prototype.canNodeUpdate = function(id)

View File

@ -1,12 +1,13 @@
var _ = require('lodash'); var _ = require('lodash');
var d3 = require('d3'); var d3 = require('d3');
var MAX_HISTORY = 1000; var MAX_HISTORY = 2000;
var MAX_PEER_PROPAGATION = 40; var MAX_PEER_PROPAGATION = 40;
var MIN_PROPAGATION_RANGE = 0; var MIN_PROPAGATION_RANGE = 0;
var MAX_PROPAGATION_RANGE = 10000; var MAX_PROPAGATION_RANGE = 10000;
var MAX_UNCLES = 1000;
var MAX_UNCLES_PER_BIN = 25; var MAX_UNCLES_PER_BIN = 25;
var MAX_BINS = 40; var MAX_BINS = 40;
@ -14,88 +15,110 @@ var History = function History(data)
{ {
this._items = []; this._items = [];
this._callback = null; this._callback = null;
var item = {
height: 0,
block: {
number: 0,
hash: '0x?',
arrived: 0,
received: 0,
propagation: 0,
difficulty: 0,
gasUsed: 0,
transactions: [],
uncles: []
},
propagTimes: []
};
} }
History.prototype.add = function(block, id) History.prototype.add = function(block, id, trusted, addingHistory)
{ {
var changed = false; var changed = false;
if( !_.isUndefined(block) && !_.isUndefined(block.number) && !_.isUndefined(block.uncles) && !_.isUndefined(block.transactions) && !_.isUndefined(block.difficulty) && block.number > 0 ) if( !_.isUndefined(block) && !_.isUndefined(block.number) && !_.isUndefined(block.uncles) && !_.isUndefined(block.transactions) && !_.isUndefined(block.difficulty) && block.number > 0 )
{ {
var historyBlock = this.search(block.number); var historyBlock = this.search(block.number);
var forkIndex = -1;
var now = _.now(); var now = _.now();
block.trusted = trusted;
block.arrived = now; block.arrived = now;
block.received = now; block.received = now;
block.propagation = 0; block.propagation = 0;
block.fork = 0;
if( historyBlock ) if( historyBlock )
{ {
// We already have a block with this height in collection
// Check if node already checked this block height
var propIndex = _.findIndex( historyBlock.propagTimes, { node: id } ); var propIndex = _.findIndex( historyBlock.propagTimes, { node: id } );
// Check if node already check a fork with this height
forkIndex = compareForks(historyBlock, block);
if( propIndex === -1 ) if( propIndex === -1 )
{ {
block.arrived = historyBlock.block.arrived; // Node didn't submit this block before
block.received = now; if( forkIndex >= 0 && !_.isUndefined(historyBlock.forks[forkIndex]) )
block.propagation = now - historyBlock.block.received; {
// Found fork => update data
block.arrived = historyBlock.forks[forkIndex].arrived;
block.propagation = now - historyBlock.forks[forkIndex].received;
}
else
{
// No fork found => add a new one
forkIndex = historyBlock.forks.push(block) - 1;
historyBlock.forks[forkIndex].fork = forkIndex;
}
// Push propagation time
historyBlock.propagTimes.push({ historyBlock.propagTimes.push({
node: id, node: id,
trusted: trusted,
fork: forkIndex,
received: now, received: now,
propagation: block.propagation propagation: block.propagation
}); });
changed = true;
} }
else else
{ {
block.arrived = historyBlock.block.arrived; // Node submited the block before
block.received = historyBlock.propagTimes[propIndex].received; if( forkIndex >= 0 && !_.isUndefined(historyBlock.forks[forkIndex]) )
block.propagation = historyBlock.propagTimes[propIndex].propagation;
if(historyBlock.hash !== block.hash || historyBlock.totalDifficulty !== block.totalDifficulty || historyBlock.transactions.length !== block.transactions.length)
{ {
index = _.findIndex( this._items, { height: block.number } ); // Matching fork found => update data
block.arrived = historyBlock.forks[forkIndex].arrived;
this._items[index].hash = block.hash; if( forkIndex === historyBlock.propagTimes[propIndex].fork )
this._items[index].parentHash = block.parentHash; {
this._items[index].nonce = block.nonce; // Fork index is the same
this._items[index].sha3Uncles = block.sha3Uncles; block.received = historyBlock.propagTimes[propIndex].received;
this._items[index].transactionsRoot = block.transactionsRoot; block.propagation = historyBlock.propagTimes[propIndex].propagation;
this._items[index].stateRoot = block.stateRoot; }
this._items[index].miner = block.miner; else
this._items[index].difficulty = block.difficulty; {
this._items[index].totalDifficulty = block.totalDifficulty; // Fork index is different
this._items[index].size = block.size; historyBlock.propagTimes[propIndex].fork = forkIndex;
this._items[index].extraData = block.extraData; historyBlock.propagTimes[propIndex].propagation = block.propagation = now - historyBlock.forks[forkIndex].received;
this._items[index].gasLimit = block.gasLimit; }
this._items[index].gasUsed = block.gasUsed;
this._items[index].timestamp = block.timestamp;
this._items[index].transactions = block.transactions;
this._items[index].uncles = block.uncles;
changed = true; }
else
{
// No matching fork found => replace old one
block.received = historyBlock.propagTimes[propIndex].received;
block.propagation = historyBlock.propagTimes[propIndex].propagation;
forkIndex = historyBlock.forks.push(block) - 1;
historyBlock.forks[forkIndex].fork = forkIndex;
} }
} }
if( trusted && !compareBlocks(historyBlock.block, historyBlock.forks[forkIndex]) )
{
// If source is trusted update the main block
historyBlock.forks[forkIndex].trusted = trusted;
historyBlock.block = historyBlock.forks[forkIndex];
}
block.fork = forkIndex;
changed = true;
} }
else else
{ {
// Couldn't find block with this height
// Getting previous max block
var prevBlock = this.prevMaxBlock(block.number); var prevBlock = this.prevMaxBlock(block.number);
if( prevBlock ) if( prevBlock )
@ -113,13 +136,16 @@ History.prototype.add = function(block, id)
var item = { var item = {
height: block.number, height: block.number,
block: block, block: block,
forks: [block],
propagTimes: [] propagTimes: []
} }
if( this._items.length === 0 || block.number >= (this.bestBlockNumber() - MAX_HISTORY + 1) ) if( this._items.length === 0 || (this._items.length === MAX_HISTORY && block.number > this.worstBlockNumber() && !addingHistory) || (this._items.length < MAX_HISTORY && block.number < this.bestBlockNumber()) )
{ {
item.propagTimes.push({ item.propagTimes.push({
node: id, node: id,
trusted: trusted,
fork: 0,
received: now, received: now,
propagation: block.propagation propagation: block.propagation
}); });
@ -139,6 +165,43 @@ History.prototype.add = function(block, id)
return false; return false;
} }
function compareBlocks(block1, block2)
{
if( block1.hash !== block2.hash ||
block1.parentHash !== block2.parentHash ||
block1.nonce !== block2.nonce ||
block1.sha3Uncles !== block2.sha3Uncles ||
block1.transactionsRoot !== block2.transactionsRoot ||
block1.stateRoot !== block2.stateRoot ||
block1.miner !== block2.miner ||
block1.difficulty !== block2.difficulty ||
block1.totalDifficulty !== block2.totalDifficulty ||
block1.size !== block2.size ||
block1.extraData !== block2.extraData ||
block1.gasLimit !== block2.gasLimit ||
block1.gasUsed !== block2.gasUsed ||
block1.transactions.length !== block2.transactions.length ||
block1.uncles.length !== block2.uncles.length)
return false;
return true;
}
function compareForks(historyBlock, block2)
{
if( _.isUndefined(historyBlock) )
return -1;
if( _.isUndefined(historyBlock.forks) || historyBlock.forks.length === 0 )
return -1;
for(var x = 0; x < historyBlock.forks.length; x++)
if(compareBlocks(historyBlock.forks[x], block2))
return x;
return -1;
}
History.prototype._save = function(block) History.prototype._save = function(block)
{ {
this._items.unshift(block); this._items.unshift(block);
@ -160,7 +223,7 @@ History.prototype.clean = function(max)
console.log("History items before:", this._items.length); console.log("History items before:", this._items.length);
this._items = _(this._items).filter(function(item) { this._items = _(this._items).filter(function(item) {
return item.height <= max; return (item.height <= max && item.block.trusted === false);
}).value(); }).value();
console.log("History items after:", this._items.length); console.log("History items after:", this._items.length);
@ -204,6 +267,22 @@ History.prototype.bestBlockNumber = function()
return 0; return 0;
} }
History.prototype.worstBlock = function()
{
return _.min(this._items, 'height');
}
History.prototype.worstBlockNumber = function(trusted)
{
var worst = this.worstBlock();
if( !_.isUndefined(worst.height) )
return worst.height;
return 0;
}
History.prototype.getNodePropagation = function(id) History.prototype.getNodePropagation = function(id)
{ {
var propagation = new Array( MAX_PEER_PROPAGATION ); var propagation = new Array( MAX_PEER_PROPAGATION );
@ -292,6 +371,11 @@ History.prototype.getUncleCount = function()
{ {
var uncles = _( this._items ) var uncles = _( this._items )
.sortByOrder( 'height', false ) .sortByOrder( 'height', false )
.filter(function (item)
{
return item.block.trusted;
})
.slice(0, MAX_UNCLES)
.map(function (item) .map(function (item)
{ {
return item.block.uncles.length; return item.block.uncles.length;
@ -315,6 +399,10 @@ History.prototype.getBlockTimes = function()
{ {
var blockTimes = _( this._items ) var blockTimes = _( this._items )
.sortByOrder( 'height', false ) .sortByOrder( 'height', false )
.filter(function (item)
{
return item.block.trusted;
})
.slice(0, MAX_BINS) .slice(0, MAX_BINS)
.reverse() .reverse()
.map(function (item) .map(function (item)
@ -330,6 +418,10 @@ History.prototype.getDifficulty = function()
{ {
var difficultyHistory = _( this._items ) var difficultyHistory = _( this._items )
.sortByOrder( 'height', false ) .sortByOrder( 'height', false )
.filter(function (item)
{
return item.block.trusted;
})
.slice(0, MAX_BINS) .slice(0, MAX_BINS)
.reverse() .reverse()
.map(function (item) .map(function (item)
@ -345,6 +437,10 @@ History.prototype.getTransactionsCount = function()
{ {
var txCount = _( this._items ) var txCount = _( this._items )
.sortByOrder( 'height', false ) .sortByOrder( 'height', false )
.filter(function (item)
{
return item.block.trusted;
})
.slice(0, MAX_BINS) .slice(0, MAX_BINS)
.reverse() .reverse()
.map(function (item) .map(function (item)
@ -360,6 +456,10 @@ History.prototype.getGasSpending = function()
{ {
var gasSpending = _( this._items ) var gasSpending = _( this._items )
.sortByOrder( 'height', false ) .sortByOrder( 'height', false )
.filter(function (item)
{
return item.block.trusted;
})
.slice(0, MAX_BINS) .slice(0, MAX_BINS)
.reverse() .reverse()
.map(function (item) .map(function (item)
@ -378,6 +478,10 @@ History.prototype.getAvgHashrate = function()
var blocktimeHistory = _( this._items ) var blocktimeHistory = _( this._items )
.sortByOrder( 'height', false ) .sortByOrder( 'height', false )
.filter(function (item)
{
return item.block.trusted;
})
.slice(0, 64) .slice(0, 64)
.map(function (item) .map(function (item)
{ {
@ -394,6 +498,10 @@ History.prototype.getMinersCount = function()
{ {
var miners = _( this._items ) var miners = _( this._items )
.sortByOrder( 'height', false ) .sortByOrder( 'height', false )
.filter(function (item)
{
return item.block.trusted;
})
.slice(0, MAX_BINS) .slice(0, MAX_BINS)
.map(function (item) .map(function (item)
{ {
@ -425,6 +533,10 @@ History.prototype.getCharts = function()
{ {
var chartHistory = _( this._items ) var chartHistory = _( this._items )
.sortByOrder( 'height', false ) .sortByOrder( 'height', false )
.filter(function (item)
{
return item.block.trusted;
})
.slice(0, MAX_BINS) .slice(0, MAX_BINS)
.reverse() .reverse()
.map(function (item) .map(function (item)

View File

@ -1,5 +1,6 @@
var geoip = require('geoip-lite'); var geoip = require('geoip-lite');
var _ = require('lodash'); var _ = require('lodash');
var trusted = require('./utils/config');
var MAX_HISTORY = 40; var MAX_HISTORY = 40;
var MAX_INACTIVE_TIME = 1000*60*60*4; var MAX_INACTIVE_TIME = 1000*60*60*4;
@ -7,6 +8,7 @@ var MAX_INACTIVE_TIME = 1000*60*60*4;
var Node = function(data) var Node = function(data)
{ {
this.id = null; this.id = null;
this.trusted = false;
this.info = {}; this.info = {};
this.geo = {} this.geo = {}
this.stats = { this.stats = {
@ -79,6 +81,11 @@ Node.prototype.setInfo = function(data, callback)
if( !_.isUndefined(data.ip) ) if( !_.isUndefined(data.ip) )
{ {
if( trusted.indexOf(data.ip) >= 0 )
{
this.trusted = true;
}
this.setGeo(data.ip); this.setGeo(data.ip);
} }
@ -360,7 +367,7 @@ Node.prototype.getBlockNumber = function()
Node.prototype.canUpdate = function() Node.prototype.canUpdate = function()
{ {
return this.info.canUpdateHistory || false; return (this.info.canUpdateHistory && this.trusted) || false;
} }
Node.prototype.isInactiveAndOld = function() Node.prototype.isInactiveAndOld = function()

14
lib/utils/config.js Normal file
View File

@ -0,0 +1,14 @@
var trusted = [
'54.94.239.50',
'52.16.188.185',
'52.4.40.229',
'52.4.131.128',
'52.0.243.36',
'52.4.180.23',
'52.5.60.7',
'52.5.26.21',
'52.5.25.137',
'::ffff:127.0.0.1',
];
module.exports = trusted;

View File

@ -116,12 +116,12 @@ netStatsApp.controller('StatsCtrl', function($scope, $filter, $localStorage, soc
console.log('We are scheduling a reconnect operation', opts); console.log('We are scheduling a reconnect operation', opts);
}) })
.on('data', function incoming(data) { .on('data', function incoming(data) {
socketAction(data.action, data.data); $scope.$apply(socketAction(data.action, data.data));
}); });
socket.on('init', function(data) socket.on('init', function(data)
{ {
socketAction("init", data.nodes); $scope.$apply(socketAction("init", data.nodes));
}); });
socket.on('client-latency', function(data) socket.on('client-latency', function(data)
@ -142,7 +142,10 @@ netStatsApp.controller('StatsCtrl', function($scope, $filter, $localStorage, soc
_.forEach($scope.nodes, function (node, index) { _.forEach($scope.nodes, function (node, index) {
// Init hashrate // Init hashrate
if( _.isUndefined(node.stats.hashrate) ) if( _.isUndefined(node.stats.hashrate) )
$scope.nodes[index].stats.hashrate = 0; node.stats.hashrate = 0;
// Init latency
latencyFilter(node);
// Init history // Init history
if( _.isUndefined(data.history) ) if( _.isUndefined(data.history) )
@ -152,7 +155,7 @@ netStatsApp.controller('StatsCtrl', function($scope, $filter, $localStorage, soc
} }
// Init or recover pin // Init or recover pin
$scope.nodes[index].pinned = ($scope.pinned.indexOf(node.id) >= 0 ? true : false); node.pinned = ($scope.pinned.indexOf(node.id) >= 0 ? true : false);
}); });
if( $scope.nodes.length > 0 ) if( $scope.nodes.length > 0 )
@ -281,6 +284,9 @@ netStatsApp.controller('StatsCtrl', function($scope, $filter, $localStorage, soc
if( _.isUndefined($scope.nodes[index].pinned) ) if( _.isUndefined($scope.nodes[index].pinned) )
$scope.nodes[index].pinned = false; $scope.nodes[index].pinned = false;
// Init latency
latencyFilter($scope.nodes[index]);
updateActiveNodes(); updateActiveNodes();
} }
@ -353,15 +359,19 @@ netStatsApp.controller('StatsCtrl', function($scope, $filter, $localStorage, soc
break; break;
case "latency": case "latency":
var index = findIndex({id: data.id}); if( !_.isUndefined(data.id) && !_.isUndefined(data.latency) )
if( !_.isUndefined(data.id) && index >= 0 )
{ {
var node = $scope.nodes[index]; var index = findIndex({id: data.id});
if( !_.isUndefined(node) && !_.isUndefined(node.stats) && !_.isUndefined(node.stats.latency) ) if( index >= 0 )
{ {
$scope.nodes[index].stats.latency = data.latency; var node = $scope.nodes[index];
if( !_.isUndefined(node) && !_.isUndefined(node.stats) && !_.isUndefined(node.stats.latency) && node.stats.latency !== data.latency )
{
node.stats.latency = data.latency;
latencyFilter(node);
}
} }
} }
@ -376,7 +386,7 @@ netStatsApp.controller('StatsCtrl', function($scope, $filter, $localStorage, soc
break; break;
} }
$scope.$apply(); // $scope.$apply();
} }
function findIndex(search) function findIndex(search)
@ -449,6 +459,7 @@ netStatsApp.controller('StatsCtrl', function($scope, $filter, $localStorage, soc
$scope.nodesTotal = $scope.nodes.length; $scope.nodesTotal = $scope.nodes.length;
$scope.nodesActive = _.filter($scope.nodes, function (node) { $scope.nodesActive = _.filter($scope.nodes, function (node) {
forkFilter(node);
return node.stats.active == true; return node.stats.active == true;
}).length; }).length;
@ -481,8 +492,49 @@ netStatsApp.controller('StatsCtrl', function($scope, $filter, $localStorage, soc
{ {
if( $scope.nodes.length ) if( $scope.nodes.length )
{ {
var bestBlock = _.max($scope.nodes, function (node) { var chains = {};
return parseInt(node.stats.block.number); var maxScore = 0;
_($scope.nodes)
.map(function (item)
{
maxScore += (item.trusted ? 50 : 1);
if( _.isUndefined(chains[item.stats.block.number]) )
chains[item.stats.block.number] = [];
if( _.isUndefined(chains[item.stats.block.number][item.stats.block.fork]) )
chains[item.stats.block.number][item.stats.block.fork] = {
fork: item.stats.block.fork,
count: 0,
trusted: 0,
score: 0
};
if(item.stats.block.trusted)
chains[item.stats.block.number][item.stats.block.fork].trusted++;
else
chains[item.stats.block.number][item.stats.block.fork].count++;
chains[item.stats.block.number][item.stats.block.fork].score = chains[item.stats.block.number][item.stats.block.fork].trusted * 50 + chains[item.stats.block.number][item.stats.block.fork].count;
})
.value();
$scope.maxScore = maxScore;
$scope.chains = _.reduce(chains, function (result, item, key)
{
result[key] = _.max(item, 'score');
return result;
}, {});
var bestBlock = _.max($scope.nodes, function (node)
{
if( $scope.chains[node.stats.block.number].fork === node.stats.block.fork && $scope.chains[node.stats.block.number].score / $scope.maxScore >= 0.5 )
{
return parseInt(node.stats.block.number);
}
return 0;
}).stats.block.number; }).stats.block.number;
if( bestBlock !== $scope.bestBlock ) if( bestBlock !== $scope.bestBlock )
@ -497,4 +549,64 @@ netStatsApp.controller('StatsCtrl', function($scope, $filter, $localStorage, soc
} }
} }
} }
function forkFilter(node)
{
if( _.isUndefined(node.readable) )
node.readable = {};
if( $scope.chains[node.stats.block.number].fork === node.stats.block.fork && $scope.chains[node.stats.block.number].score / $scope.maxScore >= 0.5 )
{
node.readable.forkClass = 'hidden';
node.readable.forkMessage = '';
return true;
}
if( $scope.chains[node.stats.block.number].fork !== node.stats.block.fork )
{
node.readable.forkClass = 'text-danger';
node.readable.forkMessage = 'Wrong chain.<br/>This chain is a fork.';
return false;
}
if( $scope.chains[node.stats.block.number].score / $scope.maxScore < 0.5)
{
node.readable.forkClass = 'text-warning';
node.readable.forkMessage = 'May not be main chain.<br/>Waiting for more confirmations.';
return false;
}
}
function latencyFilter(node)
{
if( _.isUndefined(node.readable) )
node.readable = {};
if( _.isUndefined(node.stats) ) {
node.readable.latencyClass = 'text-danger';
node.readable.latency = 'offline';
}
if (node.stats.active === false)
{
node.readable.latencyClass = 'text-danger';
node.readable.latency = 'offline';
}
else
{
if (node.stats.latency <= 100)
node.readable.latencyClass = 'text-success';
if (node.stats.latency > 100 && node.stats.latency <= 1000)
node.readable.latencyClass = 'text-warning';
if (node.stats.latency > 1000)
node.readable.latencyClass = 'text-danger';
node.readable.latency = node.stats.latency + ' ms';
}
}
}); });

View File

@ -184,7 +184,7 @@ block content
th th
i.icon-bulb(data-toggle="tooltip", data-placement="top", title="Up-time", ng-click="orderTable(['-stats.uptime'], false)") i.icon-bulb(data-toggle="tooltip", data-placement="top", title="Up-time", ng-click="orderTable(['-stats.uptime'], false)")
tbody(ng-cloak) tbody(ng-cloak)
tr(ng-repeat='node in nodes | orderBy:predicate track by node.id', class="{{ node.stats | mainClass : bestBlock }}") tr(ng-repeat='node in nodes | orderBy:predicate track by node.id', class="{{ node.stats | mainClass : bestBlock }}", id="node_{{node.id}}")
td.td-nodecheck td.td-nodecheck
i(ng-click="pinNode(node.id)", class="{{ node.pinned | nodePinClass }}", data-toggle="tooltip", data-placement="right", data-original-title="Click to {{ node.pinned ? 'un' : '' }}pin") i(ng-click="pinNode(node.id)", class="{{ node.pinned | nodePinClass }}", data-toggle="tooltip", data-placement="right", data-original-title="Click to {{ node.pinned ? 'un' : '' }}pin")
td.nodeInfo(rel="{{node.id}}") td.nodeInfo(rel="{{node.id}}")
@ -194,12 +194,15 @@ block content
i.icon-warning-o i.icon-warning-o
td td
div.small(ng-bind-html="node.info.node | nodeVersion") div.small(ng-bind-html="node.info.node | nodeVersion")
td(class="{{ node.stats | latencyClass }}") td(class="{{ node.readable.latencyClass }}")
span.small {{node.stats | latencyFilter}} span.small {{ node.readable.latency }}
td(class="{{ node.stats.mining | hashrateClass : node.stats.active }}", ng-bind-html="node.stats.hashrate | hashrateFilter : node.stats.mining") td(class="{{ node.stats.mining | hashrateClass : node.stats.active }}", ng-bind-html="node.stats.hashrate | hashrateFilter : node.stats.mining")
td(class="{{ node.stats.peers | peerClass : node.stats.active }}", style="padding-left: 11px;") {{node.stats.peers}} td(class="{{ node.stats.peers | peerClass : node.stats.active }}", style="padding-left: 11px;") {{node.stats.peers}}
td(style="padding-left: 15px;") {{node.stats.pending}} td(style="padding-left: 15px;") {{node.stats.pending}}
td(class="{{ node.stats | blockClass : bestBlock }}") {{'#'}}{{ node.stats.block.number | number }} td(class="{{ node.stats | blockClass : bestBlock }}")
span(class="{{ node.readable.forkMessage ? node.readable.forkClass : '' }}") {{'#'}}{{ node.stats.block.number | number }}
a.small(data-toggle="tooltip", data-placement="top", data-html="true", data-original-title="{{ node.readable.forkMessage }}", class="{{ node.readable.forkClass }}")
i.icon-warning-o
td(class="{{ node.stats | blockClass : bestBlock }}") td(class="{{ node.stats | blockClass : bestBlock }}")
span.small {{node.stats.block.hash | hashFilter}} span.small {{node.stats.block.hash | hashFilter}}
td(class="{{ node.stats | blockClass : bestBlock }}") td(class="{{ node.stats | blockClass : bestBlock }}")