Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Bech32m and the insertion flaw in Bech32 (BIP350, 2020)#
In 2019 it was found that Bech32 has a blind spot: when a string ends in
p, inserting or deleting q characters just before that p leaves
the checksum valid. q is the value 0, and p is 1, the constant the
checksum must leave, so the extra zeros multiply the polynomial by powers
of x without changing its remainder. Witness version 0 survives, because
its programs must be 20 or 32 bytes, but Taproot’s version 1 and later
allow other lengths. Pieter Wuille’s Bech32m changes only the constant,
and is required from witness version 1 on.
import matplotlib.pyplot as plt
import blockchainkit as bk
A string ending in p#
def ending_in_p(encoding):
for n in range(1_000):
data = [n % 32, n // 32 % 32, 7, 3, 1]
text = bk.wallets.bech32_encode("bc", data, encoding)
if text.endswith("p"):
return text
raise ValueError("no string found")
def accepts(text):
try:
bk.wallets.bech32_decode(text)
except ValueError:
return False
return True
original = ending_in_p("bech32")
print("original:", original)
inserted = {}
for encoding in ("bech32", "bech32m"):
text = ending_in_p(encoding)
inserted[encoding] = [accepts(text[:-1] + "q" * k + "p") for k in range(1, 9)]
print(f"{encoding:8} with 1..8 q's inserted:", inserted[encoding])
assert all(inserted["bech32"]) and not any(inserted["bech32m"])
original: bc1sq8rpprd0lp
bech32 with 1..8 q's inserted: [True, True, True, True, True, True, True, True]
bech32m with 1..8 q's inserted: [False, False, False, False, False, False, False, False]
Why version 0 was safe#
program = bytes(20)
v0 = bk.wallets.segwit_encode("bc", 0, program)
assert bk.wallets.segwit_decode("bc", v0).program == program
taproot = bk.wallets.segwit_encode("bc", 1, bytes(32))
print("version 0:", v0)
print("version 1:", taproot, bk.wallets.bech32_decode(taproot).encoding)
fig, ax = plt.subplots(figsize=(6, 4))
k = list(range(1, 9))
ax.plot(k, inserted["bech32"], "o-", color="#dc2626", label="Bech32")
ax.plot(k, inserted["bech32m"], "o-", color="#2563eb", label="Bech32m")
ax.set(
xlabel="q characters inserted before the final p",
ylabel="accepted",
yticks=[0, 1],
yticklabels=["no", "yes"],
)
ax.set_title("A new constant closes the insertion hole")
ax.legend()
fig.tight_layout()
plt.show()

version 0: bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq9e75rs
version 1: bc1pqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqqenm bech32m
Exercise#
Does deleting a q before a final p fool Bech32 too? Find a string
ending in qp and try.
A worked solution is in Exercises: wallets.
Total running time of the script: (0 minutes 0.049 seconds)