SHA-256 for Developers: How We Turn Quantum Noise Into Deterministic Seeds

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

This post explains SHA-256 from first principles, then shows exactly how we use it in our pipeline — with complete Python code you can run yourself.

Quantum Genesis NFT #29 — first piece generated from IBM Quantum ibm_fez

1. What Is a Hash Function?

A cryptographic hash function takes an input of any size and produces a fixed-size output. Three properties make it useful:

  • Deterministic — The same input always produces the same output. Always. On any machine, in any language, forever.
  • One-way — Given the output, you cannot reconstruct the input. There's no "un-hash" function.
  • Collision-resistant — It's computationally infeasible to find two different inputs that produce the same output.

Think of it as a fingerprint for data. Just as no two people share fingerprints (in practice), no two inputs share hash outputs (in practice). But unlike fingerprints, hash functions are perfectly deterministic — run them 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:

hashlib.sha256("hello quantum world".encode()).hexdigest()
# 7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069

hashlib.sha256("hello quantum World".encode()).hexdigest()  # capital W
# completely different: a1b2c3... (every character changes)

This "avalanche effect" — where a tiny input change causes a massive output change — is a core design property of SHA-256.

2. SHA-256 Explained

SHA-256 is a member of the SHA-2 family, designed by the NSA and published by NIST in 2001. The "256" refers to the output size: 256 bits, or 64 hexadecimal characters.

Key Specifications

Property Value
Output size 256 bits (32 bytes, 64 hex characters)
Block size 512 bits
Rounds 64
Collision resistance 2^128 operations (birthday attack)
Preimage resistance 2^256 operations
Status (2026) Secure, widely used

Internally, SHA-256 processes data in 512-bit blocks through 64 rounds of mixing operations (bitwise rotations, shifts, XOR, addition modulo 2^32). The input is padded to a multiple of 512 bits, then each block updates an internal state of eight 32-bit words. The final state is the hash.

You don't need to understand the internals to use it — Python's hashlib handles everything — but it helps to know that the algorithm is designed to make every output bit depend on every input bit.

3. Why Quantum Art Needs Deterministic Seeds

Here's our problem. When we run a quantum circuit on IBM's ibm_fez processor 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
}

This is a probability distribution — the same circuit run again would produce similar but not identical counts. We can't use the raw counts directly as a seed because they'd be slightly different each run.

But we can freeze them. Once we record the measurement results, we hash them to produce a deterministic seed. The same recorded counts will always produce the same hash, which always produces the same art.

This gives us the best of both worlds:

  • Quantum randomness — The distribution came from genuine quantum mechanical events.
  • Deterministic reproduction — Given the recorded measurements, anyone can verify the art by re-generating it from the same seed.

4. Our Process: Measurement Counts to Hex Seed

Here's the exact pipeline we use in Quantum Genesis:

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. Dictionary ordering in Python 3.7+ is insertion-order, but measurement results might arrive in any order. By sorting keys before serializing, we guarantee the same measurements always produce the same JSON string, and therefore the same hash.

Quantum Genesis NFT #50 — generated from IBM Quantum measurements

5. Using hashlib in Python

Python's hashlib module is part of the standard library — no pip install needed. Here's what you need to know:

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()

Important: Encoding Matters

Hash functions operate on bytes, not strings. You must encode your string 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()

The encoding matters. UTF-8 and ASCII produce the same bytes for English text, but differ for Unicode characters. Always specify explicitly — we use utf-8 throughout.

6. The QuantumRNG Class

The 64-character hex seed from SHA-256 gives us 256 bits of entropy. But our art generator needs many random decisions: colors, positions, sizes, rotations. We need a random number generator seeded from this hash.

We use SHA-512 for the internal PRNG to squeeze more bits from each hash 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 is not a standard PRNG like Python's random module — it's a cryptographic PRNG. Every output depends on the SHA-512 hash chain, which means:

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

7. Hash Chaining for More Randomness

A single SHA-256 gives us 256 bits. For generating complex art with hundreds of random decisions, we need more. Hash chaining solves this:

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 colon-separated counter ({current}:{i}) ensures each link in the chain is unique even if we somehow got the same hash twice. This is the same principle used in key derivation functions like HKDF.

Why Not Just Use Python's random Module?

We could do random.seed(hash_value) and use Python's Mersenne Twister. But there's a problem: the Mersenne Twister implementation could change between Python versions. Our QuantumRNG class uses only SHA-512, which is standardized. The same seed will produce the same art on Python 3.8, 3.12, or any future version — even in other languages that implement SHA-512.

8. Integrity Verification with Certificates

Each NFT in Quantum Genesis includes a Certificate of Quantum Authenticity. SHA-256 is used to create an integrity hash that links all certificate fields together:

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

If anyone modifies any field in the certificate — the processor name, the timestamp, the seed — the integrity hash won't match. This provides tamper-evidence without needing a blockchain (though our certificates are also referenced in the on-chain metadata).

9. Security Properties That Matter for Art

Not all hash properties are equally important for our use case. Here's what matters and what doesn't:

Critical for Us

  • Determinism — Same measurements must always produce the same seed, and therefore the same art. This is the foundation of reproducibility.
  • Avalanche effect — Even slightly different measurement results (from different quantum runs) should produce completely different seeds and completely different art.
  • Collision resistance — Two different sets of measurements should never produce the same seed. With 2^128 collision resistance, this is effectively guaranteed.

Nice to Have

  • Preimage resistance — Nobody can reverse-engineer the measurements from the seed. This protects the raw quantum data.
  • Second preimage resistance — Nobody can find alternative measurements that produce the same seed. This ensures each seed is uniquely tied to its quantum origin.

Not Relevant

  • Speed — We hash once per NFT. Even if SHA-256 took a full second (it takes microseconds), it wouldn't matter.
  • Quantum resistance — Grover's algorithm reduces SHA-256 preimage resistance from 2^256 to 2^128, which is still astronomically secure. Ironically, the quantum computers generating our art are nowhere near powerful enough to threaten SHA-256.
Quantum Genesis NFT #88 — generated from IBM Quantum measurements
The beautiful irony: We use quantum computers to generate randomness, then feed it through a classical cryptographic function (SHA-256) that is secure even 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 generating one Quantum Genesis 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 appears twice: once to create the seed (step 2) and once to seal the certificate (step 5). Both uses leverage the same properties — determinism and tamper-evidence — but for different purposes.

@media (prefers-color-scheme: dark) { background:#1a1a2e; border-color:#2d2d4a; box-shadow:0 1px 3px rgba(0,0,0,0.3); }
Marcelo Santos
Marcelo Santos
Engenheiro Quântico • Artista Generativo • Founder Quantum Art Lab

Desenvolvo sistemas que usam hardware quântico real (IBM Quantum, Origin Quantum) para gerar arte e NFTs com proveniência verificável on-chain. Escrevo sobre computação quântica aplicada, criptografia, Web3 e arte generativa — tudo com código que roda em processadores quânticos de verdade.

Comentários

Postagens mais visitadas deste blog

How to List NFTs on OpenSea: The Complete 2026 Guide

Polygon vs Ethereum for NFTs: Why We Chose Polygon (and Saved $490)

Quantum Error Correction Explained: Why Your Qubits Need Backup