• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jonathanbier / forkmonitor / 19027253827

28 Oct 2025 03:20PM UTC coverage: 74.233% (+0.5%) from 73.721%
19027253827

push

github

Sjors
Normalize getblock verbosity and restore Bitcoin Core 0.9.2 support

- introduce a shared GetBlockVerbosity enum/resolver so each client maps symbols to the RPC payload its node version expects
- omit the verbose argument for Core < 0.10, preventing Bitcoin Core 0.9.2 from rejecting getblock calls
- update higher-level code and specs to pass descriptive verbosity symbols, keeping behavior consistent and readable

98 of 114 new or added lines in 6 files covered. (85.96%)

45 existing lines in 2 files now uncovered.

1766 of 2379 relevant lines covered (74.23%)

371.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.17
/app/models/node.rb
1
# frozen_string_literal: true
2

3
class Node < ApplicationRecord
4✔
4
  include ::TxIdConcern
4✔
5
  include ::BitcoinUtil
4✔
6
  include ::RpcConcern
4✔
7

8
  class NoMatchingNodeError < StandardError; end
4✔
9

10
  class NoTxIndexError < StandardError; end
4✔
11

12
  nilify_blanks only: [:mirror_rpchost]
4✔
13

14
  before_save :clear_chaintips, if: :will_save_change_to_enabled?
4✔
15
  before_destroy :clear_references
4✔
16
  after_commit :expire_cache
4✔
17

18
  belongs_to :block, optional: true
4✔
19
  has_many :chaintips, dependent: :destroy
4✔
20
  has_many :blocks_first_seen, class_name: 'Block', foreign_key: 'first_seen_by_id', dependent: :nullify
4✔
21
  has_many :invalid_blocks, dependent: :restrict_with_exception
4✔
22
  has_many :inflated_blocks, dependent: :restrict_with_exception
4✔
23
  has_many :lag_a, class_name: 'Lag', foreign_key: 'node_a_id', dependent: :destroy
4✔
24
  has_many :lag_b, class_name: 'Lag', foreign_key: 'node_b_id', dependent: :destroy
4✔
25
  has_many :tx_outsets, dependent: :destroy
4✔
26
  belongs_to :mirror_block, optional: true, class_name: 'Block'
4✔
27
  has_one :active_chaintip, -> { where(status: 'active') }, class_name: 'Chaintip'
50✔
28
  has_many :softforks, dependent: :destroy
4✔
29

30
  scope :bitcoin_core_by_version, lambda {
4✔
31
                                    where(enabled: true, client_type: :core).where.not(version: nil).order(version: :desc)
5✔
32
                                  }
33
  scope :bitcoin_core_unknown_version, -> { where(enabled: true, client_type: :core).where(version: nil) }
18✔
34
  scope :bitcoin_alternative_implementations, -> { where(enabled: true).where.not(client_type: :core) }
46✔
35

36
  # Enum is stored as an integer, so do not remove entries from this list:
37
  enum client_type: { core: 0, bcoin: 1, knots: 2, btcd: 3, libbitcoin: 4, abc: 5, sv: 6, bu: 7,
4✔
38
                      omni: 8, blockcore: 9 }
39

40
  def name_with_version
4✔
41
    BitcoinUtil::Version.name_with_version(name, version, version_extra, client_type.to_sym)
7,465✔
42
  end
43

44
  def as_json(options = nil)
4✔
45
    fields = %i[
2✔
46
      id
47
      unreachable_since
48
      mirror_unreachable_since
49
      ibd
50
      client_type
51
      pruned
52
      txindex
53
      os
54
      cpu
55
      ram
56
      storage
57
      checkpoints
58
      cve_2018_17144
59
      released
60
      sync_height
61
      link
62
      link_text
63
      mempool_count
64
      mempool_bytes
65
      mempool_max
66
      mirror_ibd
67
      to_destroy
68
    ]
69
    fields << :id << :rpchost << :mirror_rpchost << :rpcport << :mirror_rpcport << :rpcuser << :rpcpassword << :version_extra << :name << :enabled if options && options[:admin]
2✔
70
    super({ only: fields }.merge(options || {})).merge({
2✔
71
                                                         height: active_chaintip&.block&.height,
72
                                                         name_with_version: name_with_version,
73
                                                         tx_outset: tx_outset,
74
                                                         last_tx_outset: tx_outsets.last,
75
                                                         has_mirror_node: mirror_rpchost.present?,
76
                                                         bip9_softforks: softforks.where(fork_type: :bip9), # rubocop:disable Naming/VariableNumber
77
                                                         bip8_softforks: softforks.where(fork_type: :bip8) # rubocop:disable Naming/VariableNumber
78
                                                       })
79
  end
80

81
  def client
4✔
82
    @client ||= if python
23,706✔
83
                  BitcoinClientPython.new(id, name_with_version, client_type.to_sym,
3,635✔
84
                                          version)
85
                else
86
                  client_klass.new(id, name_with_version,
54✔
87
                                   client_type.to_sym, version, rpchost, rpcport, rpcuser, rpcpassword)
88
                end
89
    @client
23,706✔
90
  end
91

92
  def mirror_client
4✔
93
    return nil unless mirror_rpchost
574✔
94

95
    @mirror_client ||= if python
573✔
96
                         BitcoinClientPython.new(id, name_with_version,
25✔
97
                                                 client_type.to_sym, version)
98
                       else
99
                         client_klass.new(id, name_with_version,
9✔
100
                                          client_type.to_sym, version, mirror_rpchost, mirror_rpcport, rpcuser, rpcpassword)
101
                       end
102
    @mirror_client
573✔
103
  end
104

105
  def tx_outset
4✔
106
    tx_outsets.find_by(block: block)
2✔
107
  end
108

109
  # Update database with latest info from this node
110
  def poll!
4✔
111
    if libbitcoin?
312✔
112
      block_height = client.getblockheight
×
113
      if block_height.nil?
×
114
        update unreachable_since: unreachable_since || DateTime.now
×
115
        return
×
116
      end
117
      header = client.getblockheader(block_height)
×
118
      best_block_hash = header['hash']
×
119
    elsif btcd?
312✔
120
      begin
121
        blockchaininfo = client.getblockchaininfo
3✔
122
        info = client.getinfo
3✔
123
      rescue BitcoinUtil::RPC::Error
124
        update unreachable_since: unreachable_since || DateTime.now
×
125
        return
×
126
      end
127
    elsif core? && version.present? && version < 100_000
309✔
128
      begin
129
        info = client.getinfo
×
130
      rescue BitcoinUtil::RPC::Error
131
        update unreachable_since: unreachable_since || DateTime.now
×
132
        return
×
133
      end
134
    elsif core? && version.present?
309✔
135
      begin
136
        blockchaininfo = client.getblockchaininfo
308✔
137
        networkinfo = client.getnetworkinfo
306✔
138
      rescue BitcoinUtil::RPC::Error
139
        update unreachable_since: unreachable_since || DateTime.now
2✔
140
        return
2✔
141
      end
142
    else # Version is not known the first time
143
      begin
144
        blockchaininfo = client.getblockchaininfo
1✔
145
        networkinfo = client.getnetworkinfo
1✔
146
      rescue BitcoinUtil::RPC::Error
147
        begin
148
          info = client.getinfo
×
149
        rescue BitcoinUtil::RPC::Error
150
          update unreachable_since: unreachable_since || DateTime.now
×
151
          return
×
152
        end
153
      end
154
    end
155

156
    best_block_hash ||= if blockchaininfo.present?
310✔
157
                          blockchaininfo['bestblockhash']
310✔
158
                        else
159
                          info.present? ? client.getblockhash(info['blocks']) : nil
×
160
                        end
161

162
    raise 'Best block hash unexpectedly nil' if best_block_hash.blank?
310✔
163

164
    if networkinfo.present?
310✔
165
      update(version: BitcoinUtil::Version.parse(networkinfo['version'], client_type.to_sym), peer_count: networkinfo['connections'])
307✔
166
    elsif info.present?
3✔
167
      update(version: BitcoinUtil::Version.parse(info['version'], client_type.to_sym), peer_count: info['connections'])
3✔
168
    end
169

170
    if libbitcoin?
310✔
171
      ibd = block_height < 631_885
×
172
    elsif blockchaininfo.present?
310✔
173
      block_height = blockchaininfo['blocks']
310✔
174
      ibd = if blockchaininfo.key?('initialblockdownload')
310✔
175
              blockchaininfo['initialblockdownload']
292✔
176
            elsif blockchaininfo.key?('verificationprogress')
18✔
177
              blockchaininfo['verificationprogress'] < 0.9999
18✔
178
            else
179
              # Don't set too tight because it will silence node behind warnings
180
              info['blocks'] < Block.maximum(:height) - 1000
×
181
            end
182
    end
183
    update ibd: ibd, sync_height: ibd ? block_height : nil
310✔
184

185
    # Get soft fork info using getdeploymentinfo for Bitcoin Core v23.0 and up
186
    if core? && version.present? && version >= 230_000
310✔
187
      begin
188
        deploymentinfo = client.getdeploymentinfo
292✔
189
        Softfork.process_deploymentinfo(self, deploymentinfo)
292✔
190
      rescue BitcoinUtil::RPC::Error
191
        update unreachable_since: unreachable_since || DateTime.now
×
192
        return
×
193
      end
194
    # Get soft fork info for older nodes
195
    elsif blockchaininfo.present?
18✔
196
      Softfork.process(self, blockchaininfo) if core? || knots?
18✔
197
    end
198

199
    mempool_bytes = nil
310✔
200
    mempool_count = nil
310✔
201
    mempool_max = nil
310✔
202
    if supports_getmempoolinfo?
310✔
203
      begin
204
        mempool_info = client.getmempoolinfo
310✔
205
        mempool_bytes = mempool_info['bytes']
310✔
206
        mempool_count = mempool_info['size']
310✔
207
        mempool_max = mempool_info['maxmempool']
310✔
208
      rescue BitcoinUtil::RPC::TimeOutError
209
        # Ignore the occasional timeout
210
      rescue BitcoinUtil::RPC::MethodNotFoundError
UNCOV
211
        Rails.logger.info "Skipping getmempoolinfo for #{name_with_version} (id=#{id}): RPC not available" unless Rails.env.test?
×
212
      end
213
    end
214

215
    has_tx_index = nil
310✔
216
    has_coinstatsindex_index = nil
310✔
217
    if core? && version >= 210_000
310✔
218
      index_info = client.getindexinfo
292✔
219
      has_tx_index = index_info.key? 'txindex'
292✔
220
      has_coinstatsindex_index = index_info.key? 'coinstatsindex'
292✔
221
    end
222

223
    # Mark node as reachable (if needed) before trying to fetch additional info
224
    # such as the coinbase message.
225
    update polled_at: Time.zone.now, unreachable_since: nil if unreachable_since
310✔
226

227
    block = self.ibd ? nil : Block.find_or_create_block_and_ancestors!(best_block_hash, self, false, true)
310✔
228

229
    update(
310✔
230
      polled_at: Time.zone.now,
231
      unreachable_since: nil,
232
      block: block,
233
      mempool_bytes: mempool_bytes,
234
      mempool_count: mempool_count,
235
      mempool_max: mempool_max,
236
      txindex: has_tx_index.nil? ? txindex : has_tx_index, # Set by admin before v0.21
310✔
237
      coinstatsindex: has_coinstatsindex_index
238
    )
239
  end
240

241
  # Get most recent block height from mirror node
242
  def poll_mirror!
4✔
243
    return if mirror_rpchost.nil?
63✔
244
    return unless core?
62✔
245

246
    Rails.logger.info 'Polling mirror node...'
62✔
247
    begin
248
      blockchaininfo = mirror_client.getblockchaininfo
62✔
249
    rescue BitcoinUtil::RPC::Error
UNCOV
250
      Rails.logger.info 'Failed to poll mirror node...'
×
251
      # Ignore failure
UNCOV
252
      return
×
253
    end
254
    best_block_hash = blockchaininfo['bestblockhash']
62✔
255
    ibd = blockchaininfo['initialblockdownload']
62✔
256
    block = ibd ? nil : Block.find_or_create_block_and_ancestors!(best_block_hash, self, true, nil)
62✔
257
    update mirror_block: block, last_polled_mirror_at: Time.zone.now, mirror_ibd: ibd
62✔
258
  end
259

260
  # Should be run after polling all nodes, otherwise it may find false positives
261
  def check_if_behind!(node)
4✔
262
    # Return nil if other node is in IBD:
263
    return nil if node.ibd
38✔
264

265
    # Return nil if this node is in IBD:
266
    return nil if ibd
37✔
267

268
    # Return nil if this node has no peers:
269
    return nil if peer_count.nil? || peer_count.zero?
36✔
270

271
    # Return nil if either node is unreachble:
272
    return nil if unreachable_since || node.unreachable_since
21✔
273

274
    behind = nil
20✔
275
    lag_entry = Lag.find_by(node_a: self, node_b: node)
20✔
276

277
    return nil if block.nil? || node.block.nil?
20✔
278

279
    return nil if block.work.nil? || node.block.work.nil?
20✔
280

281
    blocks_behind = nil
20✔
282
    # Use chaintips for Bitcoin Core nodes, and a simpler heuristic for other nodes
283
    if core?
20✔
284
      return nil if active_chaintip.nil? || node.active_chaintip.nil?
17✔
285

286
      # Sometimes the work field is missing:
287
      return nil if active_chaintip.block.work.nil? || node.active_chaintip.block.work.nil?
17✔
288

289
      # Not behind if at the same block
290
      if active_chaintip.block == node.active_chaintip.block
17✔
UNCOV
291
        behind = false
×
292
      # Compare work:
293
      elsif active_chaintip.block.work < node.active_chaintip.block.work
17✔
294
        behind = true
17✔
295
      end
296

297
      blocks_behind = node.active_chaintip.block.height - active_chaintip.block.height
17✔
298
    else
299
      # Not behind if at the same block
300
      if block == node.block
3✔
UNCOV
301
        behind = false
×
302
      # Compare work:
303
      elsif block.work < node.block.work
3✔
304
        behind = true
3✔
305
      end
306

307
      blocks_behind = node.block.height - block.height
3✔
308
    end
309

310
    # Allow 1 block extra for 0.16 nodes and older:
311
    return nil if core? && version < 169_999 && blocks_behind < 2
20✔
312

313
    # Allow 1 block extra for btcd and Knots nodes:
314
    return nil if (btcd? || knots?) && blocks_behind < 2
19✔
315

316
    # Allow 10 blocks extra for libbitcion nodes:
317
    return nil if libbitcoin? && blocks_behind < 10
18✔
318

319
    # Remove entry if no longer behind
320
    if lag_entry && !behind
18✔
321
      lag_entry.destroy
×
UNCOV
322
      return nil
×
323
    end
324

325
    if behind
18✔
326
      if !lag_entry
18✔
327
        # Store when we first discover the lag
328
        lag_entry = Lag.create(node_a: self, node_b: node, blocks: blocks_behind)
11✔
329
      elsif lag_entry.blocks < blocks_behind
7✔
330
        # Update block count, but only if it increased:
331
        lag_entry.update blocks: blocks_behind
2✔
332
      end
333
    end
334

335
    # Return false if behind but still in grace period:
336
    return false if lag_entry && ((Time.zone.now - lag_entry.created_at) < (ENV.fetch('LAG_GRACE_PERIOD') { (1 * 60) }).to_i)
36✔
337

338
    if lag_entry
7✔
339
      # Mark as ready to publish on RSS
340
      lag_entry.update publish: true
7✔
341

342
      # Send email after grace period
343
      unless lag_entry.notified_at
7✔
344
        lag_entry.update notified_at: Time.zone.now
6✔
345
        User.all.find_each do |user|
6✔
346
          UserMailer.with(user: user, lag: lag_entry).lag_email.deliver
6✔
347
        end
348
      end
349
    end
350

351
    lag_entry
7✔
352
  end
353

354
  def check_versionbits!
4✔
355
    return nil if ibd
26✔
356

357
    reload # Block parent links may be stale otherwise
25✔
358

359
    threshold = Rails.env.test? ? 2 : ENV['VERSION_BITS_THRESHOLD'].to_i || 50
25✔
360

361
    block = self.block
25✔
362
    return nil if block.nil?
25✔
363

364
    until_height = block.height - (VersionBit::WINDOW - 1)
25✔
365

366
    versions_window = []
25✔
367

368
    while block.present? && block.height >= until_height
25✔
369
      if block.version.blank?
74✔
370
        Rails.logger.error "Missing version for block #{block.height}"
×
UNCOV
371
        return nil
×
372
      end
373

374
      versions_window.push(block.version_bits)
74✔
375
      block = block.parent
74✔
376
    end
377

378
    return nil if versions_window.length != VersionBit::WINDOW # Less than 100 blocks or missing parent info
25✔
379

380
    versions_tally = versions_window.transpose.map(&:sum)
24✔
381
    throw "Unexpected versions_tally = #{versions_tally.length} != 29" if versions_tally.length != 29
24✔
382
    current_alerts = VersionBit.where(deactivate: nil).index_by(&:bit)
24✔
383
    known_softforks = Softfork.all.collect(&:bit).uniq
24✔
384
    versions_tally.each_with_index do |tally, bit|
24✔
385
      next if known_softforks.include?(bit)
696✔
386

387
      if tally >= threshold
696✔
388
        if current_alerts[bit].nil?
8✔
389
          Rails.logger.info "Bit #{bit} exceeds threshold"
6✔
390
          current_alerts[bit] = VersionBit.create(bit: bit, activate: self.block)
6✔
391
        end
392
      elsif tally.zero?
688✔
393
        current_alert = current_alerts[bit]
665✔
394
        if current_alert.present?
665✔
395
          Rails.logger.info "Turn off alert for bit #{bit}"
1✔
396
          current_alert.update deactivate: self.block
1✔
397
        end
398
      end
399

400
      # Send email
401
      current_alert = current_alerts[bit]
696✔
402
      next unless current_alert && !current_alert.deactivate && !current_alert.notified_at
696✔
403

404
      User.all.find_each do |user|
6✔
405
        UserMailer.with(user: user, bit: bit, tally: tally, window: VersionBit::WINDOW,
6✔
406
                        block: self.block).version_bits_email.deliver
407
      end
408
      current_alert.update notified_at: Time.zone.now
6✔
409
    end
410
  end
411

412
  private
4✔
413

414
  def supports_getmempoolinfo?
4✔
415
    return false if libbitcoin?
313✔
416
    return false if (core? || knots?) && version.present? && version < 100_000
312✔
417

418
    true
311✔
419
  end
420

421
  def client_klass
4✔
422
    Rails.env.test? ? BitcoinClientMock : BitcoinClient
63✔
423
  end
424

425
  def clear_references
4✔
426
    Block.where('? = ANY(marked_valid_by)', id).find_each do |b|
2✔
427
      b.update marked_valid_by: b.marked_valid_by - [id]
1✔
428
    end
429

430
    Block.where('? = ANY(marked_invalid_by)', id).find_each do |b|
2✔
431
      b.update marked_invalid_by: b.marked_invalid_by - [id]
2✔
432
    end
433
  end
434

435
  def expire_cache
4✔
436
    Rails.cache.delete('Node.last_updated')
1,495✔
437
  end
438

439
  def clear_chaintips
4✔
UNCOV
440
    return if enabled
×
441

UNCOV
442
    Chaintip.where(node: self).destroy_all
×
443
  end
444

445
  class << self
4✔
446
    def by_version
4✔
UNCOV
447
      where(enabled: true).order(version: :desc)
×
448
    end
449

450
    def with_mirror
4✔
UNCOV
451
      where(enabled: true, client_type: :core).where.not(mirror_rpchost: nil).order(version: :desc)
×
452
    end
453

454
    def poll!(options = {})
4✔
455
      bitcoin_core_by_version.each do |node|
14✔
456
        next if options[:unless_fresh] && node.polled_at.present? && node.polled_at > 5.minutes.ago
28✔
457

458
        Rails.logger.info "Polling node #{node.id} (#{node.name_with_version})..."
28✔
459
        node.poll!
28✔
460
      end
461

462
      bitcoin_core_unknown_version.each do |node|
14✔
UNCOV
463
        next if options[:unless_fresh] && node.polled_at.present? && node.polled_at > 5.minutes.ago
×
464

UNCOV
465
        Rails.logger.info "Polling node #{node.id} (unknown verison)..."
×
466
        node.poll!
×
467
      end
468

469
      bitcoin_alternative_implementations.each do |node|
14✔
UNCOV
470
        next if options[:unless_fresh] && node.polled_at.present? && node.polled_at > 5.minutes.ago
×
471
        # Skip libbitcoin in repeat poll, due to ZMQ socket errors
UNCOV
472
        next if options[:repeat] && node.client_type.to_sym == :libbitcoin
×
473

UNCOV
474
        Rails.logger.info "Polling node #{node.id} (#{node.name_with_version})..."
×
UNCOV
475
        node.poll!
×
476
      end
477

478
      check_chaintips!
14✔
479
      StaleCandidate.check!
14✔
480

481
      check_laggards!(options)
14✔
482

483
      bitcoin_core_by_version.first.check_versionbits!
14✔
484
    end
485

486
    def poll_repeat!(options = {})
4✔
487
      # Trap ^C
488
      Signal.trap('INT') do
1✔
UNCOV
489
        Rails.logger.info "\nShutting down gracefully..."
×
UNCOV
490
        exit # rubocop:disable Rails/Exit
×
491
      end
492

493
      # Trap `Kill `
494
      Signal.trap('TERM') do
1✔
UNCOV
495
        Rails.logger.info "\nShutting down gracefully..."
×
UNCOV
496
        exit # rubocop:disable Rails/Exit
×
497
      end
498

499
      loop do
1✔
500
        Rails.logger.info 'Polling nodes...'
1✔
501
        sleep 5
1✔
502

503
        poll!(options.merge({ repeat: true }))
1✔
504

505
        if Rails.env.test?
1✔
506
          break
1✔
507
        else
UNCOV
508
          sleep 0.5
×
509
        end
510
      end
511
    end
512

513
    def newest_node
4✔
514
      Node.newest(:core)
1✔
515
    end
516

517
    # Find pool name for a block. For modern nodes it uses getrawtransaction
518
    # with a blockhash argument, so a txindex is not required.
519
    # For older nodes it could process the raw block instead of using getrawtransaction,
520
    # but that has not been implemented.
521
    def get_coinbase_for_block!(block, block_info = nil)
4✔
522
      node = nil
3,503✔
523
      begin
524
        # getrawtransaction supports blockhash as of version 0.16, perhaps earlier too
525
        node = Node.first_newer_than(160_000, :core)
3,503✔
526
      rescue Node::NoMatchingNodeError
UNCOV
527
        Rails.logger.warn 'Unable to find suitable node in get_coinbase_for_block'
×
UNCOV
528
        return nil
×
529
      end
530
      if node.nil?
3,503✔
531
        Rails.logger.warn 'Unable to find suitable node in get_coinbase_for_block'
×
532
        return nil
×
533
      end
534
      begin
535
        block_info ||= node.getblock(block.block_hash, :summary)
3,503✔
536
      rescue BitcoinUtil::RPC::BlockPrunedError, BitcoinUtil::RPC::BlockNotFoundError
UNCOV
537
        return nil
×
538
      rescue BitcoinUtil::RPC::Error
UNCOV
539
        logger.error "Unable to fetch block #{block.block_hash} from #{node.name_with_version} while looking for pool name"
×
UNCOV
540
        return nil
×
541
      end
542
      return nil if block_info['height'].nil? # Can't fetch the genesis coinbase
3,503✔
543
      return nil if block_info['tx'].nil? || block_info['tx'].to_a.empty?
3,502✔
544

545
      if block_info['tx'].first.instance_of? String
3,420✔
546
        tx_id = block_info['tx'].first
3,420✔
547
        begin
548
          node.getrawtransaction(tx_id, true, block.block_hash)
3,420✔
549
        rescue BitcoinUtil::RPC::TxNotFoundError
550
          nil
3,414✔
551
        end
552
      else
UNCOV
553
        block_info['tx'].first
×
554
      end
555
    end
556

557
    def set_pool_for_block!(block, block_info = nil)
4✔
558
      coinbase = get_coinbase_for_block!(block, block_info)
3,503✔
559
      return if coinbase.nil? || coinbase == {}
3,503✔
560

561
      block.pool = Block.pool_from_coinbase_tx(coinbase)
6✔
562
      block.total_fee = ((coinbase['vout'].sum do |vout|
6✔
563
                            vout['value']
6✔
564
                          end * 100_000_000.0) - block.max_inflation) / 100_000_000.0
565
      if block.pool.nil?
6✔
566
        coinbase_message = Block.coinbase_message(coinbase)
6✔
567
        return if coinbase_message.nil?
6✔
568

569
        block.coinbase_message = coinbase_message.unpack('H*')
×
570
      end
UNCOV
571
      block.save if block.changed?
×
572
    end
573

574
    def rollback_checks_repeat!
4✔
575
      # Trap ^C
576
      Signal.trap('INT') do
3✔
UNCOV
577
        Rails.logger.info "\nShutting down gracefully..."
×
UNCOV
578
        exit # rubocop:disable Rails/Exit
×
579
      end
580

581
      # Trap `Kill `
582
      Signal.trap('TERM') do
3✔
UNCOV
583
        Rails.logger.info "\nShutting down gracefully..."
×
UNCOV
584
        exit # rubocop:disable Rails/Exit
×
585
      end
586

587
      loop do
3✔
588
        # TODO: find_missing shouldn't need to use a mirror node, but the current
589
        #       pattern of disconecting is not ideal for the main node.
590
        Block.find_missing(40_000, 20) # waits 20 seconds for blocks
3✔
591
        InflatedBlock.check_inflation!({ max: 10 })
3✔
592
        Node.where(client_type: :core).where.not(mirror_rpchost: nil).find_each do |node|
3✔
593
          # validate_forks! relies on the mirror node having been polled by check_inflation!
594
          Chaintip.validate_forks!(node, 50)
3✔
595
        end
596

597
        if Rails.env.test?
3✔
598
          break
3✔
599
        else
600
          sleep 0.5
×
601
        end
602
      end
603
    end
604

605
    def heavy_checks_repeat!
4✔
606
      # Trap ^C
607
      Signal.trap('INT') do
2✔
UNCOV
608
        Rails.logger.info "\nShutting down gracefully..."
×
UNCOV
609
        exit # rubocop:disable Rails/Exit
×
610
      end
611

612
      # Trap `Kill `
613
      Signal.trap('TERM') do
2✔
UNCOV
614
        Rails.logger.info "\nShutting down gracefully..."
×
UNCOV
615
        exit # rubocop:disable Rails/Exit
×
616
      end
617

618
      loop do
2✔
619
        Block.match_missing_pools!(3)
2✔
620
        StaleCandidate.process!
2✔
621
        StaleCandidate.prime_cache
2✔
622
        Softfork.notify!
2✔
623
        Node.destroy_if_requested
2✔
624

625
        if Rails.env.test?
2✔
626
          break
2✔
627
        else
UNCOV
628
          sleep 0.5
×
629
        end
630
      end
631
    end
632

633
    def check_chaintips!
4✔
634
      Chaintip.check!(bitcoin_core_by_version + bitcoin_alternative_implementations)
14✔
635
      InvalidBlock.check!
14✔
636
    end
637

638
    # Deleting a node takes very long, causing a timeout when done from the admin panel
639
    def destroy_if_requested
4✔
UNCOV
640
      Node.where(to_destroy: true).limit(1).each do |node|
×
UNCOV
641
        Rails.logger.info "Deleting node #{node.id}: #{node.name_with_version}"
×
UNCOV
642
        node.destroy
×
643
      end
644
    end
645

646
    # Sometimes an empty chaintip is left over
647
    def prune_empty_chaintips!
4✔
648
      Chaintip.includes(:node).where(nodes: { id: nil }).destroy_all
28✔
649
    end
650

651
    def check_laggards!(options = {})
4✔
652
      Lag.where('created_at < ?', 1.day.ago).destroy_all
14✔
653
      core_nodes = bitcoin_core_by_version
14✔
654
      core_nodes.drop(1).each do |node|
14✔
655
        lag = node.check_if_behind!(core_nodes.first)
14✔
656
        Rails.logger.info "Check if #{node.name_with_version} is behind #{core_nodes.first.name_with_version}... #{lag.present?}"
14✔
657
      end
658

659
      bitcoin_alternative_implementations.each do |node|
14✔
UNCOV
660
        next if options[:repeat] && node.client_type.to_sym == :libbitcoin
×
661

UNCOV
662
        lag  = node.check_if_behind!(core_nodes.first)
×
UNCOV
663
        Rails.logger.info "Check if #{node.name_with_version} is behind #{core_nodes.first.name_with_version}... #{lag.present?}"
×
664
      end
665
    end
666

667
    # Also marks ancestor blocks valid
668
    def fetch_ancestors!(until_height)
4✔
669
      node = Node.bitcoin_core_by_version.first
1✔
670
      throw 'Node in Initial Blockchain Download' if node.ibd
1✔
671
      node.block.find_ancestors!(node, false, true, until_height)
1✔
672
    end
673

674
    def first_with_txindex(client_type = :core)
4✔
675
      Node.find_by(txindex: true, client_type: client_type, ibd: false,
4✔
676
                   enabled: true) or raise BitcoinUtil::RPC::NoTxIndexError
677
    end
678

679
    def newest(client_type)
4✔
680
      Node.where(client_type: client_type, unreachable_since: nil, ibd: false,
1✔
681
                 enabled: true).order(version: :desc).first or raise NoMatchingNodeError
682
    end
683

684
    def first_newer_than(version, client_type)
4✔
685
      Node.where(client_type: client_type, unreachable_since: nil,
3,498✔
686
                 ibd: false, enabled: true).find_by('version >= ?', version) or raise NoMatchingNodeError
687
    end
688

689
    def last_updated_cached
4✔
690
      Rails.cache.fetch('Node.last_updated') do
1✔
691
        order(updated_at: :desc).first
1✔
692
      end
693
    end
694
  end
695
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc