Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions app/javascript/packs/forkMonitorApp/components/blockInfo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ class BlockInfo extends React.Component {
<br/>
Miner timestamp: <Moment format="YYYY-MM-DD HH:mm:ss" tz="UTC" parse="X">{this.props.block.timestamp}</Moment> UTC
<br/>
First seen: <Moment format="HH:mm:ss" tz="UTC">{this.props.block.created_at}</Moment> UTC
<br/>
{ this.props.block.pool &&
<span>
Mined by: { this.props.block.pool }
Expand Down
22 changes: 22 additions & 0 deletions app/javascript/packs/forkMonitorApp/components/node.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';

import Moment from 'react-moment';
import 'moment-timezone';

import {
BreadcrumbItem,
Expand Down Expand Up @@ -33,6 +34,27 @@ class Node extends React.Component {
<NodeBehind chaintip={ this.props.chaintip } node={ this.props.node } />
</td>
<td align="right">
{ this.props.chaintip && this.props.chaintip.block && this.props.node.block_first_seen_at &&
(() => {
const times = this.props.chaintip.nodes
.map(n => n.block_first_seen_at)
.filter(Boolean)
.map(t => new Date(t).getTime());
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const myTime = new Date(this.props.node.block_first_seen_at).getTime();
let color = 'black';
if (times.length > 1) {
if (myTime === minTime) color = 'green';
else if (myTime === maxTime) color = 'red';
}
return (
<span style={{ color }}>
First seen: <Moment format="HH:mm:ss" tz="UTC">{ this.props.node.block_first_seen_at }</Moment> UTC
</span>
);
})()
}
{ this.props.node.has_mirror_node &&
<NodeInflation
node={ this.props.node }
Expand Down
7 changes: 7 additions & 0 deletions app/models/block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class RollbackError < StandardError; end
has_many :sweep_transactions, dependent: :destroy
has_many :transactions, dependent: :destroy
has_many :chaintips, dependent: :destroy
has_many :node_blocks, dependent: :destroy

# Used to trigger and restore reorgs on the mirror node
attr_accessor :invalidated_block_hashes
Expand Down Expand Up @@ -491,6 +492,9 @@ def create_or_update_with(block_info, _use_mirror, node, mark_valid)
first_seen_by: node,
headers_only: false
)
NodeBlock.find_or_create_by(node: node, block: block) do |nb|
nb.first_seen_at = Time.current
end
if mark_valid.present?
if mark_valid == true
block.update marked_valid_by: [node.id]
Expand Down Expand Up @@ -518,6 +522,9 @@ def create_headers_only(node, height, block_hash)
first_seen_by: node,
tx_count: nil
)
NodeBlock.find_or_create_by(node: node, block: block) do |nb|
nb.first_seen_at = Time.current
end
# Fetch headers
block.fetch_header!(node)
# TODO: connect longer branches to common ancestor (fetch more headers if needed)
Expand Down
4 changes: 3 additions & 1 deletion app/models/chaintip.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class Chaintip < ApplicationRecord
def nodes_for_identical_chaintips
return nil if status != 'active'

chaintip_nodes = Chaintip.joins(:node).where('nodes.enabled = ? AND chaintips.status = ? AND chaintips.block_id = ?', true, status, block_id).order(
chaintip_nodes = Chaintip.joins(:node).where('nodes.enabled = ? AND chaintips.status = ? AND chaintips.block_id = ?', true, status, block_id)
.includes(node: :node_blocks)
.order(
client_type: :asc, name: :asc, version: :desc
)
res = chaintip_nodes.collect(&:node)
Expand Down
4 changes: 3 additions & 1 deletion app/models/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class NoTxIndexError < StandardError; end
has_many :tx_outsets, dependent: :destroy
belongs_to :mirror_block, optional: true, class_name: 'Block'
has_one :active_chaintip, -> { where(status: 'active') }, class_name: 'Chaintip'
has_many :node_blocks, dependent: :destroy
has_many :softforks, dependent: :destroy

scope :bitcoin_core_by_version, lambda {
Expand Down Expand Up @@ -74,7 +75,8 @@ def as_json(options = nil)
last_tx_outset: tx_outsets.last,
has_mirror_node: mirror_rpchost.present?,
bip9_softforks: softforks.where(fork_type: :bip9), # rubocop:disable Naming/VariableNumber
bip8_softforks: softforks.where(fork_type: :bip8) # rubocop:disable Naming/VariableNumber
bip8_softforks: softforks.where(fork_type: :bip8), # rubocop:disable Naming/VariableNumber
block_first_seen_at: active_chaintip&.block&.node_blocks&.find_by(node: self)&.first_seen_at
})
end

Expand Down
6 changes: 6 additions & 0 deletions app/models/node_block.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

class NodeBlock < ApplicationRecord
belongs_to :node
belongs_to :block
end
15 changes: 15 additions & 0 deletions db/migrate/20260305151011_create_node_blocks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class CreateNodeBlocks < ActiveRecord::Migration[6.1]
def change
create_table :node_blocks do |t|
t.references :node, null: false, foreign_key: true
t.references :block, null: false, foreign_key: true
t.datetime :first_seen_at, null: false

t.timestamps
end

add_index :node_blocks, %i[node_id block_id], unique: true
end
end