Verify the Difficulty Target on Bitcoin Block Headers

In order to mine a valid block, miners need to show proof-of-work in the form of a hash that is below a target value. Because the output of the hash fuction is a random set of bytes (which in turn can be intepreted as a random integer), the only way to get one of these really small random numbers is to try many times. Stumbling on a very small sha256 output is a (statistically speaking) proof that you tried many to get to that one.

The idea is akin to rolling 4 six-sided dice and hoping for a small total (say 7 or less). It won’t happen often, but if you roll enough times you’ll get one of those rolls. Note that how hard it is to find a number below target is simply a function of the target. If we increase the target it’s easier to find a number below it, and if we decrease it it gets harder.

But where is all that information recorded? How does a node verify that a block shows appropriate “proof-of-work”?

Block headers

Block headers contain a field called nBits, or “bits”. This field encodes the target difficulty for that block, and the hash of the block header is the value that is required to be below that target.

Take for example block 700,000. Its header can be downloaded on mempool.space:

04e0ff3feb36c62f0471cee034811019e43b14f459b50e00cea30a000000000000000000659cecf4a06ed500031b741384e87d40ce5c16c3ec8c09b09ffe4b863c218d1f282d3c61e4480f17d767c2ab

All headers contain the following fields:

version         4B
previous hash   32B
merkle root     32B
time            4B
nBits           4B
nonce           4B

In the case of block 700,000,

# version field, not used at the moment
04e0ff3f

# previous block hash, little endian
eb36c62f0471cee034811019e43b14f459b50e00cea30a000000000000000000
# value: 0000000000000000000AA3CE000EB559F4143BE419108134E0CE71042FC636EB

# merkle root
659cecf4a06ed500031b741384e87d40ce5c16c3ec8c09b09ffe4b863c218d1f

# timestamp, little endian
282d3c61
# value: 1631333672, Sat Sep 11 2021 04:14:32 UTC

# nBits (difficulty target, also called bits)
e4480f17

# nonce
d767c2ab

Get the block hash by hashing the header twice with sha256

echo "04e0ff3feb36c62f0471cee034811019e43b14f459b50e00cea30a000000000000000000659cecf4a06ed500031b741384e87d40ce5c16c3ec8c09b09ffe4b863c218d1f282d3c61e4480f17d767c2ab" | xxd -revert -plain | shasum -a 256 | xxd -revert -plain | shasum -a 256
# 59a90c771a9e84e9372b0b223485273a19ba3e0ffc9005000000000000000000

# the above is little endian, but block explorers always use the big endian version
# 0000000000000000000590fc0f3eba193a278534220b2b37e9849e1a770ca959

Verifying

The value in the nBits field is e4480f17. This value is stored in little endian, and therefore will need to be translated in big endian to be used, i.e. 17 0f 48 e4.

This is the target we need to ensure our block header hash is below. But notice that unlike the block header hash (a 32 byte value), the target is only 4 bytes long. The bits target field is actually an encoding for a number, akin to a base 256 scientific notation.

A quick reminder and example on scientific notation: 25 * 10^4 = 250,000. The exponent can be used to infer the number of 0s after the sigificand (the 25 in this case).

In a Bitcoin block’s nBits field, the first byte is the exponent, and the other three are the significand. The target value is defined as follows: significand * 256^(exponent - 3)

For block 700,000, the exponent is 17 and the significand 0f48e4. We can calculate the target value in Kotlin like so:

val target = BigDecimal(0x0f48e4.toInt()) * BigDecimal(256).pow(0x17 - 0x03)
println(target.toBigInteger().toString(16))
# f48e40000000000000000000000000000000000000000

This hex is not a 32 byte number as is, and it’s easier to compare it visually to the block header hash if we front pad the number with leading 0s:

# block hash vs target
target:     0000000000000000000590fc0f3eba193a278534220b2b37e9849e1a770ca959
block hash: 0000000000000000000f48e40000000000000000000000000000000000000000

And we see that the block hash is indeed smaller than the specified target!

Using hexadecimal notation to multipy things is not always intuitive, but we can rest assured that the results end up the same if we decide to convert all numbers into integers:

# nBits conversion to integers
0f48e4: 1001700
17:     23

# block hash conversion to integer
533150038325483295158428363731508440928336386051582297
val target = BigDecimal(1001700) * BigDecimal(256).pow(23 - 3)
println(target.toBigInteger())
// 1463986190114365453164631096931900700789347628299059200

And we find that indeed the block hash is below the target:

target:     1463986190114365453164631096931900700789347628299059200
block hash:  533150038325483295158428363731508440928336386051582297