SHA-256 for Developers: Turning Quantum Noise Into Deterministic Seeds

Every piece in Quantum Genesis starts with a quantum measurement — a probabilistic event that produces a distribution of bitstring counts. But art generation needs a single, fixed seed to reliably reproduce an image. The bridge between probabilistic quantum data and deterministic art is SHA-256.

What I want to show here isn't just what SHA-256 is. It's the specific way we use it, because the naive version has a subtle bug I hit the first time: the order of the measurement results matters for reproducibility, and if you get that wrong your "deterministic" seed stops being deterministic. This is the corrected pipeline, with complete Python code you can run yourself.

Quantum Genesis NFT #19

What a hash function is

A cryptographic hash takes input of any size and produces a fixed-size output. Three properties are what matter:

  • Deterministic — the same input always gives the same output. On any machine, in any language, forever.
  • One-way — given the output, you can't reconstruct the input. There's no "un-hash."
  • Collision-resistant — it's computationally infeasible to find two different inputs with the same output.

Think of it as a fingerprint for data. No two inputs (in practice) share a hash. But unlike a fingerprint, hashing is perfectly repeatable — run it a trillion times on the same input and you get the same output every time.

# Simple demonstration
import hashlib

data = "hello quantum world"
hash_output = hashlib.sha256(data.encode()).hexdigest()
print(hash_output)
# Always: 7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069

Change a single character — even a space — and the output changes completely. This "avalanche effect" is a core design property of SHA-256.

SHA-256 in a nutshell

SHA-256 is part of the SHA-2 family, designed by the NSA and published by NIST in 2001. The "256" is the output size: 256 bits, or 64 hex characters.

PropertyValue
Output size256 bits (32 bytes, 64 hex characters)
Block size512 bits
Rounds64
Collision resistance2^128 operations (birthday attack)
Preimage resistance2^256 operations
Status (2026)Secure, widely used

Internally it processes data in 512-bit blocks through 64 rounds of mixing — bitwise rotations, shifts, XOR, addition modulo 2^32. Every output bit ends up depending on every input bit. You never need the internals to use it — Python's hashlib handles all of it — but it's useful to know it's built so no single input bit can hide.

Why quantum art needs deterministic seeds

Here's our problem. When we run a circuit on IBM's ibm_fez with 4096 shots, we get something like this:

# Raw quantum measurement results (4096 shots, 8 qubits)
counts = {
    "00101101": 147,
    "11010010": 132,
    "01110100": 128,
    "10001011": 125,
    "11100001": 121,
    "00010110": 119,
    # ... 200+ more bitstrings with various counts
}

That's a probability distribution. Run the same circuit again and you get similar but not identical counts. We can't use raw counts directly as a seed because they'd differ each run.

But we can freeze them. Once the measurement results are recorded, we hash them into a deterministic seed. The same recorded counts always hash to the same seed, which always produces the same art. Best of both worlds:

  • Quantum randomness — the distribution came from genuine quantum mechanical events.
  • Deterministic reproduction — given the recorded measurements, anyone can re-generate the art from the same seed.

Measurement counts to hex seed

Here's the exact function we use:

import hashlib
import json

def measurements_to_seed(counts: dict) -> str:
    """
    Convert quantum measurement counts to a deterministic hex seed.

    1. Sort the counts dictionary by key (bitstring) for consistency
    2. Serialize to a canonical JSON string
    3. Hash with SHA-256
    4. Return the 64-character hex digest
    """
    # Sort ensures {"01": 5, "10": 3} and {"10": 3, "01": 5}
    # produce the same hash
    sorted_counts = dict(sorted(counts.items()))

    # json.dumps with sort_keys=True and no spaces for canonical form
    canonical = json.dumps(sorted_counts, sort_keys=True, separators=(',', ':'))

    # SHA-256 hash
    seed = hashlib.sha256(canonical.encode('utf-8')).hexdigest()

    return seed  # 64-character hex string

# Example
counts = {"00101101": 147, "11010010": 132, "01110100": 128}
seed = measurements_to_seed(counts)
print(f"Seed: {seed}")
# Seed: a1f3e7b2... (64 hex chars, always the same for these counts)

The critical detail is canonical serialization. Python dicts preserve insertion order since 3.7, but measurement results can arrive in any order. Sorting the keys first guarantees the same measurements always serialize to the same JSON string — and therefore the same hash. This is the line that kept my "deterministic" pipeline from breaking, and it's easy to miss.

Quantum Genesis NFT #50

Using hashlib in Python

hashlib is in the standard library — no pip install needed.

import hashlib

# Basic usage
h = hashlib.sha256()
h.update(b"some data")
h.update(b" more data")  # Can feed data incrementally
digest = h.hexdigest()    # 64-char hex string
raw = h.digest()          # 32 raw bytes

# One-liner
digest = hashlib.sha256(b"some data more data").hexdigest()

# SHA-512 (64-byte output, 128 hex chars)
digest_512 = hashlib.sha512(b"some data").hexdigest()

Encoding matters. Hash functions operate on bytes, not strings — you must encode before hashing:

# WRONG — will raise TypeError
hashlib.sha256("hello").hexdigest()

# CORRECT
hashlib.sha256("hello".encode('utf-8')).hexdigest()

# Also correct
hashlib.sha256(b"hello").hexdigest()

UTF-8 and ASCII produce identical bytes for English text, but diverge for Unicode. Always be explicit — we use utf-8 throughout.

The QuantumRNG class

A 64-character hex seed gives us 256 bits of entropy. But the art generator makes many decisions — colors, positions, sizes, rotations. We need a random stream seeded from that hash. We use SHA-512 internally to squeeze more bits per operation:

import hashlib
import struct

class QuantumRNG:
    """
    Deterministic random number generator seeded from quantum measurements.
    Uses SHA-512 hash chaining for internal state.
    """

    def __init__(self, seed_hex: str):
        """Initialize with a hex seed (typically from SHA-256 of measurements)."""
        self.state = hashlib.sha512(seed_hex.encode('utf-8')).digest()
        self.counter = 0

    def _next_bytes(self, n: int) -> bytes:
        """Generate n random bytes using hash chaining."""
        result = b""
        while len(result) < n:
            # Hash the current state + counter
            h = hashlib.sha512()
            h.update(self.state)
            h.update(self.counter.to_bytes(8, 'big'))
            self.state = h.digest()
            self.counter += 1
            result += self.state
        return result[:n]

    def random_float(self) -> float:
        """Return a random float in [0, 1)."""
        raw = self._next_bytes(8)
        # Convert 8 bytes to uint64, divide by max uint64
        value = struct.unpack('>Q', raw)[0]
        return value / (2**64)

    def random_int(self, low: int, high: int) -> int:
        """Return a random integer in [low, high]."""
        return low + int(self.random_float() * (high - low + 1))

    def random_choice(self, items: list):
        """Pick a random item from a list."""
        idx = self.random_int(0, len(items) - 1)
        return items[idx]

# Usage
seed = measurements_to_seed(counts)
rng = QuantumRNG(seed)

# Every call is deterministic given the same seed
color_r = rng.random_int(0, 255)   # Always the same for this seed
color_g = rng.random_int(0, 255)
color_b = rng.random_int(0, 255)
x_pos = rng.random_float() * 1000
palette = rng.random_choice(["nebula", "aurora", "plasma", "crystal"])

This isn't a standard PRNG like Python's random module — it's a cryptographic PRNG built on SHA-512 hash chaining:

  • Fully deterministic given the same seed.
  • Passes standard randomness tests (NIST SP 800-22).
  • You can't predict future outputs without knowing the internal state.

Why not just Python's random module?

We could do random.seed(hash_value) and use the Mersenne Twister. The problem: the Mersenne Twister implementation isn't guaranteed stable across Python versions. Our QuantumRNG uses only SHA-512, which is standardized. The same seed produces the same art on Python 3.8, 3.12, or any future version — even in other languages that implement SHA-512.

Hash chaining for more randomness

A single SHA-256 gives 256 bits. For art with hundreds of random decisions, when we want layered independence, hash chaining helps:

def hash_chain(seed: str, length: int) -> list:
    """
    Generate a chain of hashes from a seed.
    Each hash is derived from the previous one.
    """
    chain = []
    current = seed
    for i in range(length):
        current = hashlib.sha256(
            f"{current}:{i}".encode('utf-8')
        ).hexdigest()
        chain.append(current)
    return chain

# Generate 10 linked hashes from one seed
chain = hash_chain(seed, 10)
# chain[0] determines layer 1 of the art
# chain[1] determines layer 2
# etc.

The {current}:{i} counter keeps every link unique even if a hash repeated itself. This is the same principle behind key derivation functions like HKDF.

Integrity verification with certificates

Each NFT includes a Certificate of Quantum Authenticity. SHA-256 seals it with an integrity hash over all fields:

def generate_certificate(nft_id, quantum_seed, processor, circuit_desc,
                          timestamp, measurements):
    """Generate a Certificate of Quantum Authenticity."""

    certificate = {
        "certificate_id": f"QG-CERT-{nft_id:04d}",
        "quantum_seed": quantum_seed,
        "quantum_processor": processor,
        "circuit_description": circuit_desc,
        "measurement_timestamp": timestamp,
        "total_shots": sum(measurements.values()),
        "unique_states": len(measurements),
    }

    # Integrity hash covers all fields
    integrity_string = json.dumps(certificate, sort_keys=True,
                                   separators=(',', ':'))
    certificate["integrity_hash"] = hashlib.sha256(
        integrity_string.encode('utf-8')
    ).hexdigest()

    return certificate

# Verify certificate integrity
def verify_certificate(cert):
    """Verify that a certificate hasn't been tampered with."""
    stored_hash = cert.pop("integrity_hash")
    recalculated = hashlib.sha256(
        json.dumps(cert, sort_keys=True, separators=(',', ':')).encode('utf-8')
    ).hexdigest()
    cert["integrity_hash"] = stored_hash  # restore
    return stored_hash == recalculated

Change any field — the processor name, the timestamp, the seed — and the integrity hash stops matching. Tamper-evidence without needing a blockchain (though the certificates are also referenced in the on-chain metadata).

Which security properties actually matter for art

Not all hash properties are equal here.

Critical for us:

  • Determinism — same measurements must always give the same seed, and therefore the same art. The foundation of reproducibility.
  • Avalanche effect — even slightly different measurement results (from different runs) give completely different seeds and completely different art.
  • Collision resistance — two different measurement sets should never yield the same seed. At 2^128 collision resistance, effectively guaranteed.

Nice to have:

  • Preimage resistance — nobody can reverse the measurements out of the seed, protecting the raw quantum data.
  • Second preimage resistance — nobody can find alternative measurements that produce the same seed, keeping each seed tied to its quantum origin.

Not relevant to us:

  • Speed — we hash once per NFT. Even if it took a full second (it takes microseconds) it wouldn't matter.
  • Quantum resistance — Grover's algorithm halves SHA-256 preimage resistance from 2^256 to 2^128, still astronomically secure. The irony: the quantum computers making our art aren't within orders of magnitude of threatening SHA-256.

Quantum Genesis NFT #88

There's a beautiful symmetry here we didn't plan: we use quantum computers to generate randomness, then feed it through a classical cryptographic function that's itself secure against quantum attacks. The quantum part provides genuine physical randomness; the classical part provides deterministic reproducibility. Each does what it's best at.

Putting it all together

The full pipeline for one NFT:

# 1. Run quantum circuit (8 qubits, various gates, 4096 shots)
counts = run_quantum_circuit(backend="ibm_fez", shots=4096)

# 2. SHA-256: measurements → deterministic seed
seed = measurements_to_seed(counts)  # 64-char hex

# 3. QuantumRNG: seed → deterministic random stream
rng = QuantumRNG(seed)

# 4. Art generation: random stream → SVG → PNG
svg = generate_art(rng, nft_id=42)
png = svg_to_png(svg, width=2000, height=2000)

# 5. Certificate: SHA-256 integrity hash
cert = generate_certificate(nft_id=42, quantum_seed=seed, ...)

# 6. Upload to IPFS (art + metadata + certificate)
cid = upload_to_ipfs(png, metadata, cert)

SHA-256 shows up twice — once to create the seed (step 2) and once to seal the certificate (step 5). Same two properties, determinism and tamper-evidence, pressed into different service. Once the seed lands in a smart contract, the next layer of the story is how that contract's metadata is structured — which I walk through in the ERC-721 metadata standard.

Comments

Popular posts from this blog

Getting Your Collection Visible on OpenSea, Step by Step

Polygon versus Ethereum for an NFT contract, from the gas bills up

Quantum Error Correction, or Why Your Qubits Forget What They Were Doing