Operator tools — chain recovery & 51%-attack monitoring

Everything a b3chain seed or full-node operator needs to detect and respond to a 51%-class attack. The M-14 operator-pinned recovery RPCs landed in b3chain v1.1.3; the 51attack-watch.py monitoring daemon landed in v1.1.2. Both are live on every operator-run seed and are documented in-tree.

Status: live RPCs: 5 (M-14) Alert kinds: 8 Deploy: systemd

1. M-14 operator-pinned chain-recovery RPCs #

Five JSON-RPC methods, all available to any user whose cookie / rpcauth grants RPC access, that let the operator declare a specific block as the canonical tip. The validator then refuses to reorg past that block — even if a competing fork accumulates more proof-of-work. Persistence behaviour, bypass paths, and the full response procedure for a sustained attack are in RESPONSE-RUNBOOK-51ATTACK.md.

RPCSignatureEffect
finalizeblock finalizeblock "blockhash" Pin blockhash as the canonical tip. Reorgs that would unwind past this block are rejected at AcceptBlock. Persists to blocks/finalize.dat.
unfinalizeblock unfinalizeblock "blockhash" Clear the operator pin for blockhash. Removes the persisted record. Emits a finalized_drift_unfinalized alert from the watcher daemon when observed.
parkblock parkblock "blockhash" Mark a block (and its descendants) as invalid for chain selection. Useful while diagnosing a suspicious fork without committing to finalizeblock.
unparkblock unparkblock "blockhash" Clear a previous parkblock call.
getfinalizedblockhash getfinalizedblockhash Returns the currently-pinned finalized block hash, or null if none. Read-only; what the 51attack-watch.py daemon polls every 30 seconds to compute the finalized_drift_* alerts.

Implementation pointers

Bypass paths (operator must know)

  • Datadir tamper: Deleting blocks/finalize.dat and restarting clears the operator pin. This is by design — the operator must retain control of the datadir.
  • Manual unfinalizeblock: A compromised RPC credential could clear the pin. The watcher daemon emits finalized_drift_unfinalized when this happens so the operator gets alerted immediately.
  • Out-of-band consensus: Other operators on other seeds must also pin the same hash, or the network can fork along operator boundaries. The runbook §4 covers coordination.

2. 51attack-watch.py monitoring daemon #

A long-running Python daemon that polls b3chaind's JSON-RPC interface and tails its debug.log. It emits structured JSONL alerts to stdout (and, optionally, to a webhook) when any of eight distinct attack signatures fire. The unit ships as contrib/monitoring/51attack-watch.py; full operator notes are in doc/security/51-MONITORING-OPS.md.

What it polls (RPC)

  • getblockcount, getbestblockhash — chain-tip tracking and reorg-depth measurement.
  • getblockheader — common-ancestor lookup for near_reorg_cap and long_reorg depth calculations.
  • getnetworkhashps — rolling hashrate sample used by the hashrate_sustained_drop detector.
  • getfinalizedblockhash (M-14, v1.1.3) — operator-pin parity tracking; produces the three finalized_drift_* alerts.

What it tails (debug.log)

  • InvalidChainFound / "deep reorg attempt" log lines — produces deep_reorg_log alerts.
  • B3PoW: budget exceeded log lines — produces pow_budget_storm alerts when the rate-over-window crosses --pow-budget-rate / --pow-budget-window.

The log tailer (_LogTailer in the script) is inode-aware: it survives logrotate, file rename, and file truncation without missing lines or double-counting them.

3. Alert taxonomy — 8 kinds #

Each alert kind maps to a specific row in RESPONSE-RUNBOOK-51ATTACK.md §0; every alert payload includes the alert kind, severity, the runbook's recommended_action field, and a structured extra object with the raw measurements that triggered it.

  • near_reorg_cap — a competing fork has grown deep enough to be within one block of the MAX_REORG_DEPTH consensus cap. Runbook: investigate the alternate tip; consider parkblock.
  • long_reorg — an actual reorg of depth ≥ --long-reorg-depth (default 50) just occurred. Runbook: confirm chain identity, then finalizeblock the operator-canonical tip.
  • deep_reorg_logb3chaind logged a "deep reorg attempt" event (a competing chain tried to reorg deeper than the safety cap; the node rejected it). Runbook: cross-check the seeds; coordinate with other operators before acting.
  • pow_budget_storm — the verifier wall-clock budget (the 50 ms B3PoW-Scratch ceiling) has been exceeded at ≥ --pow-budget-rate events per --pow-budget-window seconds. This is the DoS-resistance backstop; sustained storms can indicate a malformed header attack. Runbook: confirm node health, then check peers.
  • hashrate_sustained_drop — the rolling networkhashps sample has dropped by ≥ --hashrate-sustained-drop (default 70%) for at least --hashrate-sustained-window seconds (default one hour). Runbook: check pool / FPGA telemetry; this is the "miners are leaving" signal.
  • finalized_drift_persistent — the operator-pinned hash from getfinalizedblockhash has not moved forward for > 24h while the chain tip has. Usually benign (the operator hasn't repinned); flagged for auditability.
  • finalized_drift_resolved — the finalized pin has caught up to (or past) the canonical tip. Informational; closes a previous finalized_drift_persistent alert.
  • finalized_drift_unfinalized — an operator-pinned block was cleared via unfinalizeblock. Critical — this is the alert that fires when the operator pin disappears unexpectedly (compromised credential, manual misuse, datadir tamper).

4. Response runbook (§0 detection triggers) #

RESPONSE-RUNBOOK-51ATTACK.md is the single source of truth for what to do when an alert fires. Its §0 table maps every alert kind to one of three recommended actions: investigate (look at logs / peers; no consensus action yet), park (mark a suspect tip non-canonical without committing), and finalize (pin the operator-canonical tip; this is the chain-recovery action). §1–§5 cover the coordination protocol with the other seed operators, when to escalate to public disclosure, and how to unwind a finalizeblock safely once the attack subsides.

5. Attack analysis & threat model #

B3POW-51-ATTACK-ANALYSIS.md is the long-form analysis. §1–§3 model what actually happens at α ≥ 50% hashrate share — the expected fork-resolution time, the cost-of-attack curve, and why the M-14 operator-pin is the correct chain-recovery primitive instead of a DAA tweak. §4.4 compares B3Chain's posture to the ETC and ETH Classic 51% events (2019–2020), to BSV (Aug 2021), and to Bitcoin Gold (May 2018, Jan 2020) — each of those chains lost millions to double-spends because they had no operator-pin primitive and no real-time monitoring; the document explains how B3Chain's M-14 + watch-daemon stack closes that gap.

6. Operator quick-start (systemd) #

Drop the daemon under /opt/b3chain/contrib/monitoring/ (it ships in the b3chain tree at contrib/monitoring/), write the unit file below to /etc/systemd/system/b3chain-51watch.service, and systemctl enable --now b3chain-51watch.service. Full deployment notes, hardening rationale, and PagerDuty / Slack webhook details are in doc/security/51-MONITORING-OPS.md.

[Unit]
Description=B3Chain 51%-attack early-warning monitor
After=network-online.target b3chaind.service
Wants=network-online.target
Requires=b3chaind.service

[Service]
Type=simple
User=deploy
Group=deploy
Environment=PYTHONUNBUFFERED=1
Environment=WEBHOOK_URL=https://pagerduty.example.com/services/XXX/integrations/YYY
ExecStart=/usr/bin/python3 /opt/b3chain/contrib/monitoring/51attack-watch.py \
    --rpc-port 8532 \
    --datadir /var/lib/b3chain/.b3chain \
    --chain main \
    --interval 30 \
    --dedup-window 300 \
    --long-reorg-depth 50 \
    --pow-budget-rate 100 \
    --pow-budget-window 3600 \
    --hashrate-sustained-drop 0.70 \
    --hashrate-sustained-window 3600
StandardOutput=append:/var/log/b3chain/51watch.jsonl
StandardError=journal
Restart=on-failure
RestartSec=15s

# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/log/b3chain
ReadOnlyPaths=/var/lib/b3chain/.b3chain

[Install]
WantedBy=multi-user.target

The webhook URL can be any HTTPS endpoint that accepts application/json; alerts are POSTed verbatim. The daemon will not exit on webhook failure — it logs the failure to stderr (journald) and keeps running. PagerDuty Events API v2, Slack incoming webhooks, and Loki / ElasticSearch / custom JSON sinks are all directly compatible.

Smoke test

The audit suite at contrib/testing/audit/smoke-51attack-watch-e2e.py runs an end-to-end smoke test against a mock RPC and mock debug.log, exercising all 8 alert kinds. Run it before deploying any monitoring-config change:

python3 contrib/testing/audit/smoke-51attack-watch-e2e.py