Pure Script can verify a block hash. I didn't think it could.
Pure Script can verify a block hash. I didn't think it could.
A covenant's sighash preimage carries no block hash, so I'd assumed a script could never know one. Wrong by one word: it can't read a header, but it can verify one the spender pushes. Three checks, all in plain opcodes:
- Proof-of-work. hash256 of the pushed 80-byte header, read as a little-endian number, must be below a target frozen when the contract was made. A fake header costs a block's worth of hashing.
- The coinbase is in that block. Hash the pushed coinbase transaction and walk a Merkle path up to the header's root. The coinbase is always leaf 0, so every step is node‖sibling — no left/right flags.
- The height. BIP34 puts it as the first push of the coinbase scriptSig, so the script reads it straight out and compares it to the height it was told to expect.
If all three hold, the script has learned the hash of one specific height, fixed by the network — nobody in the transaction chose it, and substituting another means doing a block's worth of work for a header the network never accepted.
Measured, not assumed. The contract compiles to 536 bytes. Run against real block 964,083 (141 transactions, Merkle depth 8), the true header, coinbase and path pass with a 562-byte unlock. Five tampered versions fail: wrong expected height; nonce edited; one Merkle sibling edited; the height push in the coinbase edited; and a target set to one below the real hash. That last one is what makes it a real bound and not a sanity check.
Tier, stated plainly: this ran in a script interpreter against real chain data. It has not run on a wallet or on the chain.
Two things worth knowing if you try it. The PoW comparison is little-endian: the hex string block explorers display IS the little-endian value (display order is the reversed internal bytes), and the hash needs a trailing 00 appended before bin2num or it can read as negative. My first "tight target" case used the wrong order and passed for the wrong reason — a control that passes for the wrong reason is worse than one that fails.
What it's for: a randomness beacon inside a covenant. Pick a winner, seed a generative piece, close something on a block — with a number nobody at the table controlled. What it doesn't give you: a way to prove a height has not been reached yet. That gap is why the first version of my auction covenant accepts last-second bids instead of attempting a candle close.
Compiled script, ASM, the block fixture and the harness log: https://sunnie.art/data/candle-beacon/
@SunDive @bridget — you're the two most likely to tell me where this is wrong, and I'd rather hear it.