H-1 — PoW / Block-ID Hash Isolation (B3PoW-Scratch v1.1.1)

b3chain inherits Bitcoin's SHA-256d block-identity hash but replaces the proof-of-work hash with B3PoW-Scratch v1.1.1 — a memory-hard BLAKE3 variant with a 1 MB scratchpad. The two algorithms must never be mixed up, and every PoW check site must use the right one.

Script: audit-b3pow-isolation.py Runtime: ~2 s Status: PASS

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 from GetPoWHash() (or an already-computed *pow_hash_opt). Direct use of GetHash() is flagged.
  • Static implementation scan. The PoW path in src/primitives/block.cpp must include <crypto/b3pow_scratch.h> and call into the B3PoW port, not bare BLAKE3.
  • Header API scan. CBlockHeader in src/primitives/block.h must declare both GetHash() and the B3PoW-Scratch 4-arg GetPoWHash(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.

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:

  1. 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.
  2. If getblockhash returns 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