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.
| RPC | Signature | Effect |
|---|---|---|
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
- RPC wiring:
src/rpc/blockchain.cpp(search forfinalizeblock). - Consensus enforcement:
src/validation.cpp— the chain-selection guard and theAcceptBlockrejection live here. - On-disk persistence:
src/node/blockstorage.cpp—finalize.datread/write.
Bypass paths (operator must know)
- Datadir tamper: Deleting
blocks/finalize.datand 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 emitsfinalized_drift_unfinalizedwhen 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 fornear_reorg_capandlong_reorgdepth calculations.getnetworkhashps— rolling hashrate sample used by thehashrate_sustained_dropdetector.getfinalizedblockhash(M-14, v1.1.3) — operator-pin parity tracking; produces the threefinalized_drift_*alerts.
What it tails (debug.log)
InvalidChainFound/ "deep reorg attempt" log lines — producesdeep_reorg_logalerts.B3PoW: budget exceededlog lines — producespow_budget_stormalerts 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 theMAX_REORG_DEPTHconsensus cap. Runbook: investigate the alternate tip; considerparkblock. -
long_reorg— an actual reorg of depth ≥--long-reorg-depth(default 50) just occurred. Runbook: confirm chain identity, thenfinalizeblockthe operator-canonical tip. -
deep_reorg_log—b3chaindlogged 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-rateevents per--pow-budget-windowseconds. 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 rollingnetworkhashpssample has dropped by ≥--hashrate-sustained-drop(default 70%) for at least--hashrate-sustained-windowseconds (default one hour). Runbook: check pool / FPGA telemetry; this is the "miners are leaving" signal. -
finalized_drift_persistent— the operator-pinned hash fromgetfinalizedblockhashhas 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 previousfinalized_drift_persistentalert. -
finalized_drift_unfinalized— an operator-pinned block was cleared viaunfinalizeblock. 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
7. Source & reference links #
- Monitoring daemon: contrib/monitoring/51attack-watch.py
- Operator runbook: doc/security/RESPONSE-RUNBOOK-51ATTACK.md
- Monitoring-ops guide: doc/security/51-MONITORING-OPS.md
- Threat-model analysis: doc/security/B3POW-51-ATTACK-ANALYSIS.md
- M-14 RPC wiring: src/rpc/blockchain.cpp
- M-14 consensus enforcement: src/validation.cpp
- M-14 on-disk persistence: src/node/blockstorage.cpp
- End-to-end smoke test: contrib/testing/audit/smoke-51attack-watch-e2e.py