Bitcoin & Crypto Node Tools
A technical reference for Bitcoin node operators — covering live block monitoring, blockchain queries, halving phase analysis, threat detection, and real-time price tracking. Annotated Python and Bash tools.
Running Your Own Bitcoin Node
A Bitcoin full node independently validates every transaction and block against the consensus rules — it trusts no third party. Running your own node means you are not relying on someone else's node to tell you what the blockchain says. This matters for privacy (you don't leak your addresses to a third-party API), for security (you verify your own balance independently), and for network health (every node strengthens decentralization).
Why Monitor It?
A Bitcoin node is a long-running daemon. Without monitoring, silent failures go undetected: the node may fall behind the chain tip, peer connections may drop, mempool may fill, or malicious peers may attempt eclipse attacks. The tools in this reference turn passive node operation into active infrastructure management.
| Tool | What it solves | Why it matters |
|---|---|---|
| Block Monitor | Real-time block arrival detection | Confirms node is synced and processing new blocks. Detects chain stalls within seconds. |
| Genesis Query | Validate RPC methods and node connectivity | The genesis block (height 0) is immutable — querying it with 6 methods validates that bitcoin-cli, JSON-RPC, and REST interfaces all work correctly. |
| Halving Explorer | Inspect coinbase reward across halving boundaries | Each halving cuts miner reward in half. Verifying the actual coinbase transaction value at boundary blocks confirms the halvings encoded in the consensus rules are executing correctly. |
| Security Monitor | Peer threat detection, mempool health, chain tips | Inbound peer spikes, time drift, suspicious subversions, and forked chain tips are early signals of network attacks. Catches eclipse and Sybil attempts before they impact operations. |
| Price Ticker | Live BTC/ETH/LTC/DOGE prices in terminal | Real-time Coinbase API price feed. No browser, no notifications — clean terminal display for operators who live in the shell. |
Bitcoin Halving — What and Why
Every 210,000 blocks (~4 years), the Bitcoin protocol cuts the block subsidy in half. This is coded in the consensus rules — not administered by anyone. The halving controls Bitcoin's total supply cap of 21 million BTC and its issuance schedule. Understanding which phase a block belongs to is fundamental to analyzing miner economics and network security budget over time.
| Phase | Block range | Subsidy (BTC/block) | Approximate date |
|---|---|---|---|
| 1 | 0 – 209,999 | 50.0 | Jan 2009 – Nov 2012 |
| 2 | 210,000 – 419,999 | 25.0 | Nov 2012 – Jul 2016 |
| 3 | 420,000 – 629,999 | 12.5 | Jul 2016 – May 2020 |
| 4 | 630,000 – 839,999 | 6.25 | May 2020 – Apr 2024 |
| 5 | 840,000 – 1,049,999 | 3.125 | Apr 2024 – ~2028 |
What Is the Genesis Block?
Block height 0 — the first block ever mined by Satoshi Nakamoto on January 3, 2009. Its hash is hardcoded into every Bitcoin client. The coinbase transaction cannot be spent (a quirk of the original implementation). The block's timestamp and Merkle root are fixed constants used to validate chain continuity. Querying the genesis block via multiple RPC methods is the standard sanity check for a freshly configured node.
What Is a Peer Eclipse Attack?
An eclipse attack isolates a node by monopolizing all of its peer connections with attacker-controlled nodes. The victim node only sees the attacker's view of the chain. The security monitor watches for sudden inbound peer spikes, unusual peer subversion strings (/Satoshi:0.1.0/ style), and geographic peer concentration — all early eclipse indicators.
# Snap install (easiest — manages updates automatically) sudo snap install bitcoin-core # Verify CLI is accessible bitcoin-core.cli --version bitcoin-core.cli getblockchaininfo | jq '{chain, blocks, headers, synced:.verificationprogress}' # For manual installs — set alias if using bitcoin-cli directly alias btc="bitcoin-cli -rpcuser=user -rpcpassword=pass"
# ~/.bitcoin/bitcoin.conf server=1 rpcuser=user rpcpassword=<strong-random-password> rpcallowip=127.0.0.1 txindex=1 # required for getransaction on any tx (not just wallet) daemon=1 # run as background daemon maxconnections=40
pip3 install requests speedtest-cli # requests — Coinbase API calls (price tools) # speedtest-cli — not needed for Bitcoin tools, only for the jitter+bandwidth reference # jq is required for the bash scripts sudo apt install jq
Polls bitcoin-core.cli getblockcount every 10 seconds. When a new block is detected, fetches full block data via getblock + getblockstats and renders it in a 100-column terminal box. Also appends every new block to bitcoin_block_log.txt for historical review.
import json, subprocess, time, os from datetime import datetime, timezone BOX_WIDTH = 100 POLL_INTERVAL = 10 # seconds between height checks def run_cli(*args): # Runs bitcoin-core.cli and returns stdout string, or None on error try: r = subprocess.run(["bitcoin-core.cli", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) return r.stdout.decode().strip() except subprocess.CalledProcessError as e: print(f"[!] CLI error: {e.stderr.decode().strip()}") return None def get_current_block_height(): raw = run_cli("getblockcount") return int(raw) if raw else None def get_block_data(height): block_hash = run_cli("getblockhash", str(height)) if not block_hash: return None block_json = run_cli("getblock", block_hash, "1") return json.loads(block_json) if block_json else None def display_block(block): ts = datetime.fromtimestamp(block['time'], tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC') rows = [ ("Height", block['height']), ("Hash", block['hash']), ("Time", ts), ("Transactions", block['nTx']), ("Size (bytes)", block['size']), ("Weight", block['weight']), ("Difficulty", f"{block['difficulty']:,.0f}"), ("Merkle Root", block['merkleroot']), ("Nonce", block['nonce']), ("Version", block['versionHex']), ] border = "+" + "─" * (BOX_WIDTH - 2) + "+" print(border) print(f"│{'NEW BLOCK DETECTED'.center(BOX_WIDTH-2)}│") print(border) for label, value in rows: line = f" {label:<15}{value}" print(f"│{str(line):<{BOX_WIDTH-2}}│") print(border) def main(): print("[*] Bitcoin Block Monitor — polling every 10 seconds") last_height = None while True: height = get_current_block_height() if height and height != last_height: block = get_block_data(height) if block: display_block(block) # Append to log with open("bitcoin_block_log.txt", "a") as f: f.write(json.dumps({"height": height, "hash": block['hash'], "time": block['time'], "ntx": block['nTx']}) + "\n") last_height = height time.sleep(POLL_INTERVAL) if __name__ == "__main__": main()
The genesis block (height 0, hash 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f) is the immutable anchor of the entire Bitcoin blockchain. Querying it via six different interfaces validates that the node's RPC, JSON-RPC, and REST APIs all work. The coinbase transaction of the genesis block is unspendable — this is a known quirk of Satoshi's original implementation.
#!/usr/bin/env bash
CLI="${BITCOIN_CLI:-/snap/bin/bitcoin-core.cli}"
RPC_USER="${BITCOIN_RPC_USER:-user}"
RPC_PASS="${BITCOIN_RPC_PASS:-pass}"
RPC_HOST="127.0.0.1"
RPC_PORT="8332"
# ── Method 1: By block height via CLI (recommended) ──────────────
GENESIS_HASH=$("$CLI" getblockhash 0)
"$CLI" getblock "$GENESIS_HASH" | jq '{hash, height, time, nTx, size, difficulty, merkleroot, nonce}'
# ── Method 2: Header only (lighter / faster) ──────────────────────
"$CLI" getblockheader "$GENESIS_HASH" | jq '{hash, height, time, difficulty, mediantime, chainwork}'
# ── Method 3: Raw block data (hex) ────────────────────────────────
"$CLI" getblock "$GENESIS_HASH" 0 # verbosity 0 = raw hex serialization
# ── Method 4: Full JSON decode (verbosity 2 — all tx data) ────────
"$CLI" getblock "$GENESIS_HASH" 2 | jq '.tx[0] | {txid, locktime, vout: .vout[0]}'
# ── Method 5: JSON-RPC over HTTP (curl) ───────────────────────────
curl -s --user "$RPC_USER:$RPC_PASS" \
--data-binary '{"jsonrpc":"1.0","id":"genesis","method":"getblock","params":["'$GENESIS_HASH'",1]}' \
-H 'content-type: text/plain;' \
"http://$RPC_HOST:$RPC_PORT/" | jq '.result | {hash, height, time, nTx}'
# ── Method 6: REST API (no auth required — local only) ────────────
curl -s "http://127.0.0.1:8332/rest/block/$GENESIS_HASH.json" | jq '{hash, height, time, nTx}'
{
"hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
"height": 0,
"time": 1231006505, ← 2009-01-03 18:15:05 UTC — Satoshi's timestamp
"nTx": 1, ← one coinbase transaction only
"size": 285,
"difficulty": 1,
"merkleroot": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
"nonce": 2083236893 ← proof of work nonce
}
Iterates through the block heights at each halving boundary (209,999 → 210,000, 419,999 → 420,000, etc.) and queries the actual coinbase transaction value at each boundary. This lets you observe the protocol-enforced subsidy cut in the raw blockchain data — not from a third-party source.
#!/bin/bash # Boundary block heights — start and end of each halving epoch BLOCK_HEIGHTS=(1 209999 210000 419999 420000 629999 630000 839999 840000 1049999) RED='\033[0;31m'; GREEN='\033[0;32m'; WHITE='\033[1;37m'; NC='\033[0m' for ((i=0; i<${#BLOCK_HEIGHTS[@]}-1; i+=2)); do PHASE_START=${BLOCK_HEIGHTS[$i]} PHASE_END=${BLOCK_HEIGHTS[$((i+1))]} echo -e "${RED}Phase $((i/2+1)): Blocks $PHASE_START — $PHASE_END${NC}" # ── Fetch start block ────────────────────────────────────────── START_HASH=$(bitcoin-core.cli getblockhash "$PHASE_START" 2>/dev/null) if [[ $? -eq 0 ]]; then START_DATA=$(bitcoin-core.cli getblock "$START_HASH" 2) # coinbase value = first output of first (coinbase) transaction VALUE=$(echo "$START_DATA" | jq -r '.tx[0].vout[0].value') TS=$(echo "$START_DATA" | jq -r '.time') DATE=$(date -d @$TS '+%Y-%m-%d %H:%M:%S') echo -e " Start — height $PHASE_START | coinbase: ${WHITE}${VALUE} BTC${NC} | $DATE" fi # ── Fetch end block ──────────────────────────────────────────── END_HASH=$(bitcoin-core.cli getblockhash "$PHASE_END" 2>/dev/null) if [[ $? -eq 0 ]]; then END_DATA=$(bitcoin-core.cli getblock "$END_HASH" 2) VALUE=$(echo "$END_DATA" | jq -r '.tx[0].vout[0].value') TS=$(echo "$END_DATA" | jq -r '.time') DATE=$(date -d @$TS '+%Y-%m-%d %H:%M:%S') echo -e " End — height $PHASE_END | coinbase: ${WHITE}${VALUE} BTC${NC} | $DATE" fi sleep 1 done # Phase 5 — post-4th-halving spot checks echo -e "${RED}Phase 5: Spot checks 850000 – 880000${NC}" for HEIGHT in 850000 860000 870000 880000; do HASH=$(bitcoin-core.cli getblockhash "$HEIGHT" 2>/dev/null) || { echo " height $HEIGHT not yet mined"; continue; } DATA=$(bitcoin-core.cli getblock "$HASH" 2) VALUE=$(echo "$DATA" | jq -r '.tx[0].vout[0].value') TS=$(echo "$DATA" | jq -r '.time') echo -e " Height $HEIGHT | coinbase: ${WHITE}${VALUE} BTC${NC} | $(date -d @$TS '+%Y-%m-%d')" sleep 1 done
Intelligent dashboard that runs heuristic threat detection against the live node state. Collects data from five RPC calls — getnetworkinfo, getpeerinfo, getblockchaininfo, getmempoolinfo, getchaintips — and applies structured checks every 20 seconds. Alerts write to a log file and optionally to syslog.
Heuristic Threat Checks
| Check | Metric | Threshold | Threat it detects |
|---|---|---|---|
| Inbound peer count | getnetworkinfo.connections_in | > 50 | Sybil / eclipse attack — attacker flooding inbound slots |
| Peer spike | Δ peer count vs previous run | +15 in 20s | Sudden mass connection — early eclipse indicator |
| Time drift | getpeerinfo[].timeoffset | > ±70s per peer | Time-warp attack — manipulate median time past |
| Stale peers | getpeerinfo[].lastseen | > 3600s | Dead peers wasting connection slots |
| Suspicious subversion | getpeerinfo[].subver | Known malicious strings | Non-standard clients, probe bots, MITM proxies |
| Peer concentration | Unique /24 subnets | < 4 unique /24s | All peers from same ASN — eclipse, co-located attack |
| Chain tip divergence | getchaintips | Any non-active tips | Orphaned blocks, forks — chain split indicator |
| Debug.log errors | Tail of debug.log | ERROR / WARNING lines | Internal consensus failures, disk issues |
#!/usr/bin/env bash
BITCOIN_CLI="${BITCOIN_CLI:-/snap/bin/bitcoin-core.cli}"
LOGFILE="${HOME}/bitcoin_node_monitor.log"
UPDATE_INTERVAL=20
ALERT_SYSLOG=true
PEER_SPIKE_THRESHOLD=15
HIGH_INBOUND_THRESHOLD=50
# ── RPC data collection ───────────────────────────────────────────
NET_INFO=$( "$BITCOIN_CLI" getnetworkinfo 2>/dev/null)
PEER_INFO=$( "$BITCOIN_CLI" getpeerinfo 2>/dev/null)
CHAIN_INFO=$("$BITCOIN_CLI" getblockchaininfo 2>/dev/null)
MEM_INFO=$( "$BITCOIN_CLI" getmempoolinfo 2>/dev/null)
TIPS=$( "$BITCOIN_CLI" getchaintips 2>/dev/null)
# ── Heuristic: Inbound peer count ────────────────────────────────
INBOUND=$(echo "$NET_INFO" | jq '.connections_in // 0')
[[ "$INBOUND" -gt "$HIGH_INBOUND_THRESHOLD" ]] &&
log_alert "HIGH INBOUND PEERS: $INBOUND (threshold $HIGH_INBOUND_THRESHOLD) — possible eclipse"
# ── Heuristic: Peer spike detection (state in /tmp) ──────────────
PREV_PEERS=$(cat /tmp/btc_peer_count 2>/dev/null || echo 0)
CUR_PEERS=$(echo "$PEER_INFO" | jq 'length')
DELTA=$((CUR_PEERS - PREV_PEERS))
echo "$CUR_PEERS" > /tmp/btc_peer_count
[[ "$DELTA" -gt "$PEER_SPIKE_THRESHOLD" ]] &&
log_alert "PEER SPIKE: +$DELTA peers in ${UPDATE_INTERVAL}s — investigate immediately"
# ── Heuristic: Time drift ─────────────────────────────────────────
echo "$PEER_INFO" | jq -r '.[] | "\(.addr) \(.timeoffset)"' | while read ADDR OFFSET; do
ABS=$(echo "$OFFSET" | tr -d '-')
[[ "$ABS" -gt 70 ]] && log_alert "TIME DRIFT: $ADDR offset=${OFFSET}s — time-warp risk"
done
# ── Heuristic: Suspicious subversion strings ─────────────────────
BAD_SUBS=("/Satoshi:0.1.0/" "/Satoshi:0.0.1/" "/therealbitcoin.org/")
echo "$PEER_INFO" | jq -r '.[].subver' | while read SUB; do
for BAD in "${BAD_SUBS[@]}"; do
[[ "$SUB" == *"$BAD"* ]] && log_alert "SUSPICIOUS PEER SUBVER: $SUB"
done
done
# ── Heuristic: Chain tip divergence ──────────────────────────────
echo "$TIPS" | jq -r '.[] | "\(.status) \(.hash) height=\(.height)"' | grep -v "^active " |
while read line; do log_alert "CHAIN TIP: $line"; done
# ── Dashboard output (top peers by ping) ──────────────────────────
echo "$PEER_INFO" | jq -r 'sort_by(.pingtime) | .[:10] | .[] |
" \(.addr | .[0:40])<35 ping=\(.pingtime*1000|round)ms in=\(.bytesrecv) out=\(.bytessent)"'
Fetches live prices for BTC, ETH, LTC, and DOGE from the Coinbase public spot price API every 10 seconds. Renders inside a fixed-width terminal frame. Color-codes each price: green for up, red for down, yellow for unavailable. Press Q to quit cleanly.
https://api.coinbase.com/v2/prices/{CURRENCY}-USD/spot endpoint is public, no auth required, and returns a simple JSON object. No API key needed for spot price reads. The rate limit is generous for a 10-second polling interval.
import requests, time, shutil, os CURRENCIES = ['BTC', 'ETH', 'LTC', 'DOGE'] UPDATE_SECS = 10 FRAME_WIDTH = 44 def fetch_price(currency): try: r = requests.get(f"https://api.coinbase.com/v2/prices/{currency}-USD/spot", timeout=5) r.raise_for_status() return float(r.json()['data']['amount']) except: return None def format_change(cur, prev): if cur is None: return "\033[33mUnavailable\033[0m" if prev is None: return f"{cur:.2f}" diff = cur - prev color = "\033[32m" if diff >= 0 else "\033[31m" sign = "+" if diff > 0 else "" return f"{color}{cur:.2f} ({sign}{diff:.2f})\033[0m" def display(prev_prices): print("\033[H", end="") # cursor to top — no scroll bar = "═" * FRAME_WIDTH print(bar) print(" CRYPTO PRICES (USD)".center(FRAME_WIDTH)) print(bar) for c in CURRENCIES: price = fetch_price(c) print(f" {c:5} {format_change(price, prev_prices.get(c))}") if price: prev_prices[c] = price print(bar) print(" [Q] Quit".center(FRAME_WIDTH)) def main(): prev = {} try: while True: display(prev) time.sleep(UPDATE_SECS) except KeyboardInterrupt: print("\nExiting.") if __name__ == "__main__": main()
════════════════════════════════════════════
CRYPTO PRICES (USD)
════════════════════════════════════════════
BTC 105432.18 (+218.40)
ETH 3891.55 (-12.30)
LTC 87.42 (+0.15)
DOGE 0.3812 (+0.0021)
════════════════════════════════════════════
[Q] Quit
Minimal script — fetches prices for BTC, ETH, LTC, DOGE and outputs a JSON object to stdout. Designed to be piped into other tools or called from a parent script that needs machine-readable price data without parsing terminal display output.
import requests, json, sys CURRENCIES = ['BTC', 'ETH', 'LTC', 'DOGE'] API_TIMEOUT = 5 def fetch_price(currency): try: r = requests.get(f"https://api.coinbase.com/v2/prices/{currency}-USD/spot", timeout=API_TIMEOUT) r.raise_for_status() return float(r.json()['data']['amount']) except: return None if __name__ == "__main__": prices = {c: fetch_price(c) for c in CURRENCIES} print(json.dumps(prices, indent=2))
python3 crypto_price_json_output.py | jq . # Output: { "BTC": 105432.18, "ETH": 3891.55, "LTC": 87.42, "DOGE": 0.38 } # Get BTC price only python3 crypto_price_json_output.py | jq '.BTC'
Enhanced version of the price tracker — runs in a continuous loop with configurable update interval (default 10s), tracks previous price to display delta with color coding, and handles API errors gracefully by printing to stderr without crashing the loop. Press any key + Enter to quit.
import requests, time, sys, os, select CURRENCIES = ['BTC', 'ETH', 'LTC', 'DOGE'] API_TIMEOUT = 5 UPDATE_INTERVAL = 10 def fetch_price(currency): url = f"https://api.coinbase.com/v2/prices/{currency}-USD/spot" try: r = requests.get(url, timeout=API_TIMEOUT) r.raise_for_status() return float(r.json()['data']['amount']) except requests.exceptions.RequestException as e: print(f"Error fetching {currency}: {e}", file=sys.stderr) return None def format_change(current, previous): if current is None: return "\033[33mUnavailable\033[0m" if previous is None: return f"{current:.2f}" diff = current - previous if diff > 0: return f"\033[32m{current:.2f} (+{diff:.2f})\033[0m" elif diff < 0: return f"\033[31m{current:.2f} ({diff:.2f})\033[0m" else: return f"{current:.2f} (=)" def display(prev_prices): os.system('clear') print("═" * 44) print(f" {'Currency':<8} {'Price (USD)'}") print("─" * 44) for c in CURRENCIES: price = fetch_price(c) print(f" {c:<8} {format_change(price, prev_prices.get(c))}") if price: prev_prices[c] = price print("═" * 44) def main(): prev_prices = {} try: while True: display(prev_prices) # Non-blocking stdin check — quit if input arrives if sys.stdin in select.select([sys.stdin], [], [], UPDATE_INTERVAL)[0]: break except KeyboardInterrupt: pass print("\nExiting.") if __name__ == "__main__": main()
Blockchain & Block Data
| Command | Purpose | Key output fields |
|---|---|---|
getblockchaininfo | Full chain state | chain, blocks, headers, verificationprogress, pruned |
getblockcount | Current block height (integer) | — |
getblockhash <height> | Block hash at given height | — |
getblock <hash> [verbosity] | Block data (0=hex, 1=json, 2=full tx) | height, nTx, size, weight, time, difficulty, merkleroot |
getblockheader <hash> | Header only — no tx list (fast) | hash, height, time, difficulty, chainwork |
getchaintips | All known chain tips (forks/orphans) | status: active, valid-fork, invalid, headers-only |
getblockstats <hash> | Per-block fee and size statistics | avgfee, avgfeerate, totalfee, utxo_increase |
Network & Peers
| Command | Purpose | Key fields |
|---|---|---|
getnetworkinfo | Network config and connection counts | version, subversion, connections, connections_in, connections_out, relayfee |
getpeerinfo | Per-peer connection details | addr, subver, pingtime, timeoffset, bytessent, bytesrecv, inbound, synced_headers |
addnode <ip> add | Manually add a trusted peer | — |
disconnectnode <ip> | Disconnect a specific peer | — |
setban <ip> add | Ban an IP from connecting | — |
listbanned | Show all banned IPs and expiry | — |
Mempool
| Command | Purpose |
|---|---|
getmempoolinfo | Mempool size, bytes, usage, min fee rate |
getrawmempool | All txids currently in mempool |
getmempoolentry <txid> | Single transaction mempool status, fees, ancestors |
# Current sync status bitcoin-core.cli getblockchaininfo | jq '{blocks, headers, synced: (.verificationprogress*100|round|tostring + "%")}' # List all inbound peers with ping bitcoin-core.cli getpeerinfo | jq '[.[] | select(.inbound) | {addr, ping_ms: (.pingtime*1000|round), subver}]' # Mempool fee summary bitcoin-core.cli getmempoolinfo | jq '{size, bytes, minrelaytxfee, mempoolminfee}' # Check for chain forks bitcoin-core.cli getchaintips | jq '[.[] | select(.status != "active")]' # Current block reward (coinbase of latest block) HEIGHT=$(bitcoin-core.cli getblockcount) HASH=$(bitcoin-core.cli getblockhash $HEIGHT) bitcoin-core.cli getblock $HASH 2 | jq '.tx[0].vout[0].value' # Watch new blocks arrive (live) watch -n 10 'bitcoin-core.cli getblockcount'