1. The dual-hash architecture
CBlockHeader (80 bytes)
|
+-- GetHash() = SHA-256d(serialize(header)) --> block ID, txid context, p2p inv, header chain
|
+-- GetPoWHash(prev_block_hash, pad, budget, out_exceeded)
= b3pow_scratch(serialize(header), init_scratchpad(prev_block_hash))
--> CheckProofOfWork(...) only
Two methods, two algorithms, one block header. Every consensus
call site must pick the right one; the audit verifies this
statically and on a live regtest block. The full B3PoW-Scratch
spec is at
contrib/miner/b3miner-rtl/SPEC.md.
2. What is being audited
- Static call-site scan. Every
CheckProofOfWork(...)invocation in the source tree must pass a value derived fromGetPoWHash()(or an already-computed*pow_hash_opt). Direct use ofGetHash()is flagged. - Static implementation scan. The PoW path in
src/primitives/block.cppmust include<crypto/b3pow_scratch.h>and call into the B3PoW port, not bare BLAKE3. - Header API scan.
CBlockHeaderinsrc/primitives/block.hmust declare bothGetHash()and the B3PoW-Scratch 4-argGetPoWHash(prev_block_hash, pad, budget, out_exceeded). - Functional check. Mine a regtest block and verify:
- Re-serialising and SHA-256d-ing the header reproduces the
block ID returned by
getblockhash. b3pow_ref.b3pow_scratch(header, prev_block_hash)is a different value (the dual-hash methods are independent).- That B3PoW value (interpreted as little-endian uint256) is below the block's compact target.
- Re-serialising and SHA-256d-ing the header reproduces the
block ID returned by
3. Sub-check H-1.1 — verifier wall-clock budget
Because B3PoW-Scratch can take milliseconds to verify (vs
microseconds for SHA-256d), an adversary that submits a header
designed to be hard to disprove could burn CPU. The verifier
therefore runs under a 50 ms wall-clock budget
(consensus parameter b3pow_verify_budget_ms); if the
budget expires before a decision is reached, the result is
PoWResult::BudgetExceeded, which propagates as
BlockValidationResult::BLOCK_POW_BUDGET and routes to
Misbehaving(*peer, "b3pow-budget-exceeded").
Script:
audit-b3pow-budget.py — static checks plus
the matching C++ unit tests
(pow_hash_budget_exceeded,
CheckBlockHeaderPoW_budget_exceeded).
4. Sub-check H-1.2 — LRU scratchpad cache
The first verification against a given prev_block_hash
spends ~5 ms initialising a 1 MB scratchpad. To avoid
paying that cost again for sibling headers that share the same
parent, ChainstateManager owns a
b3pow::Cache — a thread-safe LRU keyed by
prev_block_hash, depth
consensus.b3pow_cache_depth (4 entries by default
≈ 4 MB resident).
Script:
audit-b3pow-cache.py — static wiring checks
plus the b3pow_cache_tests C++ suite, which covers
insert/retrieve, LRU eviction, distinct pads per
prev_block_hash, and thread-safety under concurrent
access.
5. Sub-check H-1.3 — HEADERS verification cap
Bitcoin's protocol allows up to 2000 headers per HEADERS message.
At 50 ms per B3PoW verification, naively verifying all 2000
would burn 100 s of CPU per malicious peer per message. To
bound that, ProcessHeadersMessage truncates the
verified subset to MAX_B3POW_VERIFY_PER_BATCH = 256
headers, capping worst-case CPU at ≈13 s per malicious
peer per message. Truncation is logged via
LogDebug(BCLog::NET, "Truncating HEADERS batch...").
Any deferred headers are fetched on the next round-trip.
Script:
audit-b3pow-headers-cap.py.
6. Why it matters
A bug here can fork the network in two distinct ways:
- If the chain accepts blocks where SHA-256d of the header is below target (instead of B3PoW-Scratch), then any pre-existing Bitcoin ASIC can instantly attack b3chain. The whole reason for B3PoW disappears.
- If
getblockhashreturns the B3PoW hash instead of the SHA-256d hash, every block explorer, light client, and Merkle-proof verifier breaks immediately.
7. How to run
cd b3chain pip3 install blake3 # for the functional check python3 contrib/testing/audit/audit-b3pow-isolation.py python3 contrib/testing/audit/audit-b3pow-budget.py python3 contrib/testing/audit/audit-b3pow-cache.py python3 contrib/testing/audit/audit-b3pow-headers-cap.py
Or run everything (Phase 11 + the three B3PoW sub-audits) via
bash contrib/testing/audit/run-all.sh.
The old script paths audit-pow-isolation.py and
audit-internal-miner-e2e.py remain as thin
deprecation shims that exec the new entry points,
so any external runner pinned to the old filenames keeps working
for one release.
8. Expected output
[H-1] PoW / Block-ID isolation (B3PoW-Scratch v1.1.1)
========================================================================
PASS [H-1] N CheckProofOfWork call site(s) all use a PoW hash
PASS [H-1] PoW path references b3pow_scratch (src/primitives/block.cpp, src/primitives/block.h)
PASS [H-1] CBlockHeader::GetHash() declared
PASS [H-1] CBlockHeader::GetPoWHash(prev_block_hash, pad, budget, out_exceeded) declared
PASS [H-1.1] BLOCK_POW_BUDGET routes to Misbehaving("b3pow-budget-exceeded")
PASS [H-1.2] ChainstateManager::m_b3pow_cache declared
PASS [H-1.3] MAX_B3POW_VERIFY_PER_BATCH caps HEADERS batches
Spawning regtest node and verifying live block hash relationships...
PASS [H-1] getblockhash() returns SHA-256d (block ID), not B3PoW
PASS [H-1] block ID and B3PoW hash are different id=68c337ef... pow=3adf8417...
PASS [H-1] mined block's B3PoW hash <= target pow_int <= target: True
------------------------------------------------------------------------
10/10 checks passed in 1.8s
AUDIT RESULT: PASS [H-1]
9. Source files
- contrib/testing/audit/audit-b3pow-isolation.py
- contrib/testing/audit/audit-b3pow-budget.py (H-1.1)
- contrib/testing/audit/audit-b3pow-cache.py (H-1.2)
- contrib/testing/audit/audit-b3pow-headers-cap.py (H-1.3)
-
src/primitives/block.cpp —
GetHashandGetPoWHashdefinitions - src/crypto/b3pow_scratch.cpp — the C++ port
-
src/crypto/b3pow_cache.cpp —
b3pow::Cache -
src/pow.cpp —
CheckProofOfWork+CheckBlockHeaderPoW -
src/net_processing.cpp — budget routing +
MAX_B3POW_VERIFY_PER_BATCH - contrib/miner/b3miner-rtl/SPEC.md — the algorithm spec