Skip to content

blockchain: validate BLAKE2b (header v2) proof of work - #1

Merged
paulscode merged 1 commit into
paulscode:blake2bfrom
Kilombino:blake2b-pow
Oct 12, 2026
Merged

paulscode merged 1 commit into
paulscode:blake2bfrom
Kilombino:blake2b-pow

Conversation

@Kilombino

Copy link
Copy Markdown

This lets the blockchain package check BLAKE2b (header v2) headers itself, following Bitcoin Knots v29.4.2, so a light client (neutrino) can follow the chain without trusting a backend. Today checkProofOfWork refuses every v2 header.

Rules, with the Knots reject reason each one mirrors:

  • chaincfg: new Blake2bHeight and Blake2bTargetShift fields, set to 961640/22 on mainnet and 150308/20 on testnet4. A height of zero means not configured, which is what every other Params gets.
  • checkProofOfWork: a v2 block id is compared against its target in the same way as a SHA256d id (Knots calls CheckProofOfWork(GetHash(), nBits) for both). The reserved flag bits 0xc0 must be clear (bad-flags-highbits).
  • CheckBlockHeaderContext: below Blake2bHeight a header must be v1 (bad-version-sha256d). From that height on it must be v2 (bad-version-blake2b) and commit to its own height (bad-header-height). On a network with no Blake2bHeight, a v2 header is still refused with ErrUnsupportedProofOfWork, as it is now.
  • calcNextRequiredDifficulty: the target of the block at Blake2bHeight is shifted once by Blake2bTargetShift, capped at the PoW limit (ApplyBlake2bTargetShift). The shift is skipped on the no-retarget and min-difficulty paths, as in GetNextWorkRequired.

Out of scope: the activation headline lives in the coinbase, so it is a block rule and not a header rule. It is left to full validation.

Tests (blockchain/blake2b_test.go) use real mainnet headers from a Knots v29.4.2 node:

  • every v2 header in wire/testdata passes PoW, and fails with another nonce;
  • the activation target, and all 7 retargets since 961632, match the chain (testdata/blake2b_retargets.json).

I also replayed the whole mainnet header chain (heights 0 to 976738, 15,099 of them v2) through CheckBlockHeaderSanity and CheckBlockHeaderContext, and every header validated. The existing blockchain, wire and chaincfg tests pass.

Related: a neutrino change to store 164-byte headers (Kilombino/neutrino, branch blake2b) and a lightning-fork PR that enables the neutrino backend on top of both. With the three together, neutrino lnd on regtest syncs across activation, opens a channel to a bitcoind-backed lnd, pays both ways and closes cooperatively.

Mirror Bitcoin Knots v29.4.2 so a light client can check BLAKE2b-chain
headers itself:

- chaincfg: Blake2bHeight and Blake2bTargetShift (mainnet 961640/22,
  testnet4 150308/20). Zero height means not configured.
- checkProofOfWork: a v2 block id is compared with its target like a
  SHA256d one; reserved flag bits 0xc0 must be clear (bad-flags-highbits).
- CheckBlockHeaderContext: v1 below Blake2bHeight (bad-version-sha256d),
  v2 from it on (bad-version-blake2b), committed height = height
  (bad-header-height). Without Blake2bHeight a v2 header is still refused
  with ErrUnsupportedProofOfWork.
- calcNextRequiredDifficulty: the activation block's target is shifted
  once by Blake2bTargetShift (ApplyBlake2bTargetShift), not on the
  no-retarget or min-difficulty paths, as in GetNextWorkRequired.

Tests use real mainnet headers: every v2 header's PoW, the activation
target and the 7 retargets since (testdata/blake2b_retargets.json).
@paulscode

Copy link
Copy Markdown
Owner

Thank you for this, it's careful work.

I checked it rule by rule against Knots v29.4.2 (tag
v29.4.2.knots20260508) and it matches: the version check on the v2 flag,
the height commitment, the reserved flag bits (rejected even with
BFNoPoWCheck, as Knots does), the id compared to the target for both header
versions, and the one-off shift. The shift lands exactly: 0x1702353d
shifted by 22 is 0x1a008d4f, the real header at 961640. Your fixture
headers all hash to their ids and link up, and all 7 retargets match.

One correction, about the comment in calcNextRequiredDifficulty and the
PR text: Knots doesn't skip the shift on the no-retarget path.
fPowNoRetargeting is checked inside CalculateNextWorkRequired, and
GetNextWorkRequired then shifts that result like any other. Only the
testnet minimum difficulty return skips it. The outcome is the same today,
because the shift caps at the proof of work limit and regtest's target is the
limit, but I'd rather the code say what Knots does.

I've added one commit on top of yours (d1bd6964):

  • the no-retarget path is marked shiftable, as in Knots;
  • a nil check, since that path also returns for the genesis block, which has
    no parent;
  • tests for testnet4's minimum difficulty rules at activation (a late block
    gets the limit, the look-back is shifted, the next block is not), for
    regtest at activation and at genesis, and for an activation that lands on
    a retarget boundary (neither real network's does, but the path exists).

I've merged your commit with that one and tagged v0.25.1-blake2b.6.

This also fixes something in my current release that I hadn't noticed.
With a pruned Knots backend, btcwallet fetches pruned blocks from peers and
runs blockchain.CheckBlockSanity on them
(chain/pruned_block_dispatcher.go:656). The btcd I ship today refuses every v2
block there, and the dispatcher then bans the peer that sent it. So a node
on a pruned Knots can't fetch any pruned block from after activation. Your
change makes that check validate BLAKE2b proof of work instead, so it goes
into my next release whether or not neutrino does.

Two small notes, nothing to change now:

  • A v2 header whose committed height is below activation is only refused by
    the contextual check. Knots refuses it in CheckBlockHeader already.
    Callers that run both checks accept the same headers either way; the
    sanity-only callers (the pruned block fetcher) ask for blocks by hash, so
    it doesn't matter there.
  • In the contextual check, the v2 checks run before the difficulty check,
    while Knots checks difficulty first. That only changes which error you get
    when a header breaks both.

@Kilombino

Copy link
Copy Markdown
Author

Thanks for the review and for d1bd696, agreed on the no-retarget path.

Some field data: Kilowallet 1.0 (Android) now ships LND with neutrino built against v0.25.1-blake2b.6 (lightning-fork#3). Phones on mainnet follow the chain from compact filters and check the v2 headers' PoW themselves; real channels are open since today, including a 2M one funded through PSBT.

On your first note: happy to send a small follow-up so the sanity check refuses a v2 header that commits to a height below activation, as Knots does in CheckBlockHeader, if you want it.

neutrino-blake2b#1 and lightning-fork#3 build on this and are ready for review whenever you have time.

@paulscode

Copy link
Copy Markdown
Owner

Good to hear Kilowallet is running it on mainnet, and thanks for the field report.

Yes please on the follow-up: refusing a v2 header that commits to a height below activation, as Knots does in CheckBlockHeader, closes the last gap between the two sanity checks. A test with a header one block below activation and one at it would be enough.

One thing worth knowing for Kilowallet: Knots is about to change the coinbase maturity rules (bitcoinknots/bitcoin#429, flag day at block 979920). A neutrino node does no transaction validation, so it can't enforce that rule itself either way, but its wallet will need to treat coinbase outputs from block 973440 on as unspendable for longer. I'm working out how to express that in btcd and lnd before the flag day and will share it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants