Skip to main content

hh - Humanized Hash (Python)

hh turns a blockchain address, a public key or any hash into a small deterministic picture that a person can compare at a glance: a 4 x 4 matrix of solid squares, circles and triangles in four colours. It exists to catch address poisoning and clipboard substitution, which work because people check only the first and last characters of a long string. The colours are chosen so that people with a colour vision deficiency can tell them apart as well.

0x1234567890abcdef00112233445566778899aabb 0x12345678f1e2d3c4b5a69788796a5b4c8899aabb
picture of the first address picture of the second address

The two addresses agree in their first and last eight hex digits. Their pictures are unrelated.

This is the Python implementation, hh-python, published on PyPI as humanized-hash. It is pure Python, depends on the standard library only, supports CPython 3.9 to 3.14 and PyPy 3.10, and produces, byte for byte, the output of the C++ reference implementation hh-cpp, which owns the specification and the golden vectors. testdata/ is a byte-identical copy of those vectors; testdata/SOURCE names the hh-cpp release they came from. The same pictures come from hh-kotlin (JVM and Android, Maven io.github.censync:hh), hh-ts (TypeScript, npm @censync/hh) and go-hh (Go, github.com/censync/go-hh).

A longer example: Sui

A Sui address has 64 hex digits, and nobody reads 64 digits. The second address below differs from the first in one digit, the third in two; the changed digits are marked. In the text they are easy to miss. The pictures and the tags are unrelated, because every cell depends on every bit of the input.

Picture Address Tag
picture of the first Sui address 0xeab3150efcb34ff74930d8f3d491be109070a39e4d380de7737aff5c72a0b6b2 B6P-65H
picture of the second Sui address 0xeab3150efcb34ff74930d8f8d491be109070a39e4d380de7737aff5c72a0b6b2 Q60-QKR
picture of the third Sui address 0xeab3150efcb34ff74930d8f3d491be109010a39e4d380dc7737aff5c72a0b6b2 ZSJ-7BK

What a forger pays, by calculation. One current GPU tries about 1.4 billion addresses per second; a try against hh also has to compute the stretched base digest, which leaves about 680 000 tries per second. The figures are the expected search times on one such GPU for a typical picture (SECURITY.md of hh-cpp has the reasoning).

The forged address has to match Tries One GPU
the first 4 and the last 4 hex digits 2^32 3 seconds
the first 6 and the last 6 hex digits 2^48 2.3 days
the first 8 and the last 8 hex digits 2^64 420 years
the universal picture, with two cells allowed to differ 2^52, stretched 210 years
the universal picture, in every cell 2^68, stretched 14 million years
the ends of the text and the picture the product of the two
the keyed picture cannot be searched: without the key the picture cannot be computed

A lookalike of the text is cheap, which is why address poisoning works. A lookalike of the picture is not, and the two costs multiply. A picture that looks the same is still strong evidence rather than proof; the tag or the full address is the check that is certain.

Properties

  • Two modes. A universal picture is the same for everyone and is what two people compare. A keyed picture is computed with a 32-byte secret of the wallet: an attacker who does not hold the key cannot compute, and therefore cannot grind, a lookalike. Inside an application keyed pictures are the default.
  • Deterministic to the byte. Integer arithmetic only. The same input gives the same pixels and the same PNG, BMP and JPEG bytes as hh-cpp, on every platform and every interpreter.
  • Frozen. The algorithm has no version and never changes; a picture that a user has learned stays the same for ever. Library releases follow SemVer and never alter the output.
  • No dependencies. dependencies = []: no Pillow, no NumPy, not even zlib. SHA-256, HMAC and PBKDF2 come from hashlib and hmac; deflate, PNG, BMP, the baseline JPEG encoder and the rasteriser are part of the library.
  • Fast enough for an interpreter. The rasteriser works on whole rows of samples with integer bit sets instead of looping over samples: on a desktop core a 128-pixel picture takes one to two milliseconds, its PNG three to four, the base digest two to four.
  • Made for colour vision deficiency. About one man in twelve does not see colours the way the rest do. The four colours were chosen for them: the palette was searched so that every pair stays apart under simulated protanopia, deuteranopia and tritanopia, and every colour keeps a contrast of 3:1 on white and on dark surfaces. Shape carries most of the information, so a picture still works in greyscale (the measurements are in docs/design of hh-cpp).
  • Pixels, not pictures. The library returns RGBA bytes and encoded files; making a Tkinter, Qt or Pillow image of them is one line in the host.
  • Typed. Type hints throughout and a py.typed marker.

Quick start

pip install humanized-hash
from humanized_hash import BaseDigest, Fingerprint

digest = BaseDigest.of_hex("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")    # slow: cache it
fingerprint = Fingerprint.universal(digest)      # or Fingerprint.keyed(digest, key)
image = fingerprint.render(128)                  # 128 x 128 RGBA pixels, image.rgba

png: bytes = image.encode_png()                  # or image.save("address.png")
tag: str = fingerprint.tag                       # "TKSPVH", shown as TKS-PVH

A keyed picture needs the 32-byte secret of the wallet:

from humanized_hash import SecretKey

with SecretKey(key_bytes) as key:                # wiped when the block ends
    private = Fingerprint.keyed(digest, key)

BaseDigest.of takes the bytes of an address, of_hex their hexadecimal spelling and of_text an address that exists only as text. BaseDigest.from_bytes computes nothing: it restores a digest that was cached as bytes(digest).

Invalid values raise HhError, a ValueError whose code is the error of the specification; arguments of the wrong type raise TypeError.

A decision (confirming a payment, verifying a pasted address) should be backed by a picture of at least 64 device-independent pixels, better 96, next to the picture it is compared with. Smaller pictures are for recognition in lists. See docs/INTEGRATION.md for web backends, Tkinter, Pillow and Qt, caching and key handling, and SECURITY.md of hh-cpp for what a picture proves and what it does not.

Command line

The package installs the command humanized-hash; python -m humanized_hash is the same tool.

humanized-hash 0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed --out address.png
humanized-hash --text bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 --size 256 --out address.png
humanized-hash <hex> --key <64 hex digits> --shape round --frame double --out private.png

It takes the options of hh_cli of hh-cpp and prints the same values. It is a demonstration and a test tool: a real host never takes a key from the command line.

Testing

Python 3.9 or newer; nothing to install.

python -m unittest discover -s tests -t .        # every test, against the source tree
tools/crosscheck.sh <path to hh_cli of hh-cpp>   # differential test against hh-cpp
python tools/bench.py                            # what each step costs on this machine
python -m build                                  # sdist and wheel (needs the build package)

The tests reproduce every record of the golden vectors, compare the rasteriser with a per-sample transcription of the specification and decode every encoder's output. The rules for patches are in CONTRIBUTING.md, the releases in CHANGELOG.md.

License

MIT, see LICENSE. Copyright (c) 2026 Dmitry Mandrika. CenSync

Release files for humanized-hash 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for humanized-hash 1.0.0
File Size Uploaded
humanized_hash-1.0.0.tar.gz 180.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for humanized-hash 1.0.0
File Interpreter ABI Platform
humanized_hash-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 224.5 kB

Release files / humanized_hash-1.0.0.tar.gz

Download URL humanized_hash-1.0.0.tar.gz
Size 180.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1e2678dd4ba838738a9422e4d5daf6a9802285f7a9be81e68939614be36afa87
BLAKE2b-256 checksum
How to use checksums
dcfd7c33240e180e5c10213894d27a67b89751c1c93351bdb722d6cb95d933e7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.12

Release files / humanized_hash-1.0.0-py3-none-any.whl

Download URL humanized_hash-1.0.0-py3-none-any.whl
Size 43.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f9360580397b73185bf0ad42535f1a6e559cfa7b8319b6680d422e2e7a0494fd
BLAKE2b-256 checksum
How to use checksums
f53c362a7a48171f3895be0e10840e77e84bdee9db41bc180efb33376e1f0128
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.12

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page