H-1 — PoW / Block-ID Hash Isolation

b3chain inherits Bitcoin's SHA-256d block-identity hash but replaces the proof-of-work hash with double BLAKE3-256. The two algorithms must never be mixed up, and every PoW check site must use the right one.

Script: audit-pow-isolation.py Runtime: ~1 s Status: 7 / 7 PASS

1. The dual-hash architecture

CBlockHeader (80 bytes)
   |
   +-- GetHash()      = SHA-256d(serialize(header))   --> block ID, txid context, p2p inv, header chain
   |
   +-- GetPoWHash()   = BLAKE3d(serialize(header))    --> CheckProofOfWork(...) only

Two methods, two algorithms, one input. Every consensus call site must pick the right one, and the audit verifies this statically and on a live regtest block.

2. What is being audited

  • Static call-site scan. Every CheckProofOfWork(...) invocation in the source tree must pass a value derived from GetPoWHash(). Direct use of GetHash() is flagged.
  • Static implementation scan. The PoW hashing implementation in src/primitives/block.cpp must reference BLAKE3.
  • Header API scan. CBlockHeader in src/primitives/block.h must declare both GetHash() and GetPoWHash().
  • Functional check. Mine a regtest block and verify:
    • Re-serialising and SHA-256d-ing the header reproduces the block ID returned by getblockhash.
    • BLAKE3d of the same bytes is a different value.
    • The BLAKE3d value (interpreted as little-endian uint256) is below the block's compact target.

3. 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 BLAKE3d), then any pre-existing Bitcoin ASIC can instantly attack b3chain. The whole reason for BLAKE3 disappears.
  2. If getblockhash returns the BLAKE3 hash instead of the SHA-256d hash, every block explorer, light client, and Merkle-proof verifier breaks immediately.

4. How to run

cd b3chain
pip3 install blake3   # for the functional check
python3 contrib/testing/audit/audit-pow-isolation.py

5. Expected output

[H-1] PoW / Block-ID hash isolation
========================================================================
  PASS  [H-1] 2 CheckProofOfWork call site(s) all use GetPoWHash
  PASS  [H-1] PoW hashing references BLAKE3 (src/primitives/block.cpp, src/primitives/block.h)
  PASS  [H-1] CBlockHeader::GetHash() declared
  PASS  [H-1] CBlockHeader::GetPoWHash() declared

  Spawning regtest node and verifying live block hash relationships...
  PASS  [H-1] getblockhash() returns SHA-256d (block ID), not BLAKE3
  PASS  [H-1] block ID and BLAKE3 PoW hash are different  id=68c337ef569de154...  pow=3adf8417d7586ce7...
  PASS  [H-1] mined block's BLAKE3 PoW hash <= target  hash_int < target: True
------------------------------------------------------------------------
  7/7 checks passed in 1.3s
AUDIT RESULT: PASS  [H-1]

6. Common pitfalls

  • Variable name confusion. The audit also matches pow_hash / powhash identifiers, but if you store the PoW hash in a generic uint256 hash variable the audit cannot tell which algorithm produced it — prefer descriptive names.
  • Forward declarations. Lines like bool CheckProofOfWork(uint256 hash, ...) are whitelisted (function parameter, not a call). The audit detects this with a regex on uint256 hash.
  • Headers chain check. Bitcoin's HeadersChainSync verifies cumulative work using the target derived from nBits, not the actual hash, so it is unaffected by this isolation.

7. Source files

The problem in one sentence

B3Chain uses two different hash functions for two purposes — SHA-256d for block IDs (so external tools and explorers stay compatible) and BLAKE3d for the proof-of-work check — and confusing them is a catastrophic, silent failure.

The theory

Every block has two hashes:

GetHash()    -> SHA-256d(serialized header)    # used as block id, txid lookup, merkle leaf
GetPoWHash() -> BLAKE3d(serialized header)     # used by ContextualCheckProofOfWork

A miner's job is to find a header with GetPoWHash() <= target. The network's job is to verify that property. If ContextualCheckProofOfWork ever calls GetHash() instead of GetPoWHash():

  • Block IDs are usually well below the target (because SHA-256 outputs

are uniform 32-byte numbers, just like BLAKE3 outputs).

  • So almost any block submitted to such a buggy node would pass PoW

validation — including blocks with no actual mining work behind them.

  • Result: the chain's PoW security collapses to "whatever the easiest

way to find a low SHA-256 hash is", which is effectively zero on modern CPUs.

Hands-on demo

python3 contrib/testing/audit/audit-pow-isolation.py

This audit does two things:

  1. Static grep over every GetHash() and GetPoWHash() call site in

src/, classifying each one as "PoW context" or "ID context" and flagging mismatches.

  1. Functional test: constructs a header where SHA-256d ≤ target but

BLAKE3d > target, submits it to a regtest node, expects rejection. Then the inverse.

Exercise

In src/validation.cpp, locate ContextualCheckProofOfWork (or CheckProofOfWork depending on the call path). Replace the call to GetPoWHash() with GetHash(). Rebuild, re-run the audit:

// Before
if (!CheckProofOfWork(block.GetPoWHash(), block.nBits, params)) ...
// After (catastrophic)
if (!CheckProofOfWork(block.GetHash(), block.nBits, params)) ...

Expected new output:

  FAIL  [H-1] static audit found PoW check using GetHash (line N of validation.cpp)
  FAIL  [H-1] header with SHA256d-only PoW accepted by regtest node
AUDIT RESULT: FAIL  [H-1]

Why "looks fine in CI" doesn't help

Most unit tests work with regtest, where nBits is set very low and both hashes pass anyway. The bug only manifests in production once a real attacker notices it. That's exactly why this audit runs the contradiction check — a header that satisfies one hash function but not the other — rather than relying on "did the node accept the block?".

Further reading

  • BIP-141 SegWit txid vs wtxid (similar dual-identity pattern):

github.com/bitcoin/bips/blob/master/bip-0141.mediawiki

  • The original BCH split confused some mining hardware about which

difficulty algorithm to use; this is the same class of bug at the hash-function level.