SC-00-Cypherpunk-Origins-Python - Les origines Cypherpunk de la blockchain

<< Retour au sommaire | Suivant : Setup Foundry >>


Objectifs d’apprentissage

  1. Comprendre le mouvement Cypherpunk et son heritage technologique
  2. Manipuler les primitives cryptographiques fondatrices (hash, signatures, PoW)
  3. Construire une mini-blockchain a la main avec Python
  4. Implementer un arbre de Merkle et comprendre son rôle
  5. Decouvrir les tables de hachage distribuees (DHT/Kademlia)

Prerequis

  • Python 3.10+ avec hashlib (stdlib)
  • pycryptodome pour les signatures numériques
  • Aucune connaissance blockchain prealable

Duree estimee : 60 minutes


1. Le Manifeste Cypherpunk

“Privacy is necessary for an open society in the electronic age.” – Eric Hughes, A Cypherpunk’s Manifesto, 1993

Le mouvement Cypherpunk, ne dans les annees 1980-90, reunit des cryptographes, mathematiciens et activistes convaincus que la cryptographie est l’outil fondamental de la liberte individuelle a l’ere numérique.

Textes fondateurs

Annee Auteur Texte Idee cle
1985 David Chaum Security without Identification Monnaie electronique anonyme
1988 Timothy May The Crypto Anarchist Manifesto Crypto = outil d’emancipation
1993 Eric Hughes A Cypherpunk’s Manifesto “Cypherpunks write code”
1997 Adam Back Hashcash Proof-of-Work anti-spam
1998 Wei Dai b-money Monnaie decentralisee théorique
2008 Satoshi Nakamoto Bitcoin whitepaper Synthese de toutes ces idees

Les smart contracts modernes (Ethereum, 2015) sont l’aboutissement direct de 30 ans d’innovations Cypherpunk. Ce notebook retrace ces briques fondatrices avec du vrai code executable.

# Filiation technologique : des Cypherpunks aux Smart Contracts
filiation = {
    "1991 - PGP (Zimmermann)":        "Chiffrement asymetrique grand public",
    "1997 - Hashcash (Back)":          "Proof-of-Work -> minage Bitcoin",
    "1998 - b-money (Dai)":            "Monnaie decentralisee theorique",
    "1999 - Napster":                  "P2P massif (centralise) -> BitTorrent",
    "2001 - BitTorrent (Cohen)":       "DHT Kademlia -> reseau Ethereum",
    "2004 - RPoW (Finney)":            "Proof-of-Work reutilisable",
    "2008 - Bitcoin (Nakamoto)":       "Hash chain + PoW + P2P + signatures",
    "2013 - Ethereum (Buterin)":       "Bitcoin + Turing-completude = Smart Contracts",
}

print("FILIATION CYPHERPUNK -> SMART CONTRACTS")
print("=" * 60)
for event, contribution in filiation.items():
    print(f"  {event}")
    print(f"    -> {contribution}")
    print()
FILIATION CYPHERPUNK -> SMART CONTRACTS
============================================================
  1991 - PGP (Zimmermann)
    -> Chiffrement asymetrique grand public

  1997 - Hashcash (Back)
    -> Proof-of-Work -> minage Bitcoin

  1998 - b-money (Dai)
    -> Monnaie decentralisee theorique

  1999 - Napster
    -> P2P massif (centralise) -> BitTorrent

  2001 - BitTorrent (Cohen)
    -> DHT Kademlia -> reseau Ethereum

  2004 - RPoW (Finney)
    -> Proof-of-Work reutilisable

  2008 - Bitcoin (Nakamoto)
    -> Hash chain + PoW + P2P + signatures

  2013 - Ethereum (Buterin)
    -> Bitcoin + Turing-completude = Smart Contracts

Observation

Chaque innovation de cette filiation repose sur un petit nombre de primitives cryptographiques : le hachage, les signatures numériques, la preuve de travail, et les reseaux pair-a-pair. Bitcoin n’a rien invente : il a combine ces briques existantes de maniere geniale.

Les sections suivantes explorent chacune de ces primitives avec du code executable.


2. Hash et integrite

Le hachage cryptographique est la brique la plus fondamentale. Une fonction de hachage transforme une donnee de taille arbitraire en une empreinte de taille fixe, avec trois proprietes :

  1. Determinisme : même entree -> même sortie, toujours
  2. Effet avalanche : 1 bit change -> sortie completement différente
  3. Resistance aux collisions : quasi-impossible de trouver deux entrees avec le même hash

SHA-256 (Secure Hash Algorithm, 256 bits) est utilise par Bitcoin et de nombreuses blockchains.

import hashlib

# SHA-256 : la brique fondamentale de Bitcoin
data = b"Cypherpunks write code"
hash_value = hashlib.sha256(data).hexdigest()
print(f"Message  : {data.decode()}")
print(f"SHA-256  : {hash_value}")
print(f"Longueur : {len(hash_value)} caracteres hex = {len(hash_value)*4} bits")
print()

# Effet avalanche : un seul caractere change
data2 = b"Cypherpunks write Code"  # 'c' -> 'C'
hash2 = hashlib.sha256(data2).hexdigest()
print(f"Message  : {data2.decode()}")
print(f"SHA-256  : {hash2}")
print()

# Compter les bits differents (distance de Hamming)
diff_bits = bin(int(hash_value, 16) ^ int(hash2, 16)).count('1')
print(f"Bits differents : {diff_bits} / 256 ({diff_bits/256*100:.1f}%)")
print("-> Un seul caractere change, ~50% des bits changent (effet avalanche)")
Message  : Cypherpunks write code
SHA-256  : 42cc22190b177e5c48e32fe87c214d88eb21cac7780aad65b8b816d77cf22820
Longueur : 64 caracteres hex = 256 bits

Message  : Cypherpunks write Code
SHA-256  : b211556e694dbc8f1c6d46e438d89cb1d99c285489c066282fdd5dfb542a646d

Bits differents : 130 / 256 (50.8%)
-> Un seul caractere change, ~50% des bits changent (effet avalanche)

Interpretation — l’effet avalanche et ses trois usages industriels

L’effet avalanche est crucial pour la securite : il est impossible de deviner le hash a partir de petites modifications de l’entree. En pratique, ~128 bits sur 256 changent (~50%), ce qui confirme que SHA-256 se comporte comme une fonction aleatoire.

Cette propriete est exploitee dans : - Bitcoin : le hash du bloc doit commencer par N zeros (Proof-of-Work) - Ethereum : Keccak-256 pour les adresses et les signatures - Git : SHA-1 pour identifier les commits (SHA-256 en migration)

Exercice : Schema de commitment avec hash

Un commitment scheme permet de s’engager sur une valeur sans la reveler. Par exemple, Alice veut s’engager sur un choix secret sans que Bob puisse le deviner.

Le schema est le suivant : 1. Alice choisit un secret (ex: son vote) et un nonce aleatoire 2. Elle calcule commitment = SHA256(nonce + secret) et le publie 3. Plus tard, elle revele nonce et secret : Bob verifie que SHA256(nonce + secret) == commitment

Indices : - Utilisez hashlib.sha256() et hexdigest() - Le nonce peut etre un entier aleatoire converti en string - La concatenation nonce + secret forme le message a hacher

# Exercice : Schema de commitment avec hash
import hashlib

def commit(secret, nonce):
    """Creer un commitment hash(nonce + secret).
    
    TODO etudiant : retourner le hash SHA-256 de la concatenation nonce + secret.
    """
    return None  # TODO etudiant : implementer

def verify(secret, nonce, commitment):
    """Verifier qu'un (secret, nonce) correspond au commitment.
    
    TODO etudiant : recalculer le hash et comparer avec commitment.
    Retourner True si identique, False sinon.
    """
    return False  # TODO etudiant : implementer

print("Exercice a completer")
Exercice a completer

3. Chaîne de hash (proto-blockchain)

L’idee centrale de Bitcoin est de chainer les blocs par leur hash : chaque bloc contient le hash du bloc précédent. Modifier un ancien bloc invalide tous les blocs suivants.

Ce concept existait déjà dans les travaux de Stuart Haber et Scott Stornetta (1991) sur l’horodatage de documents numériques.

import hashlib
import json
from datetime import datetime

def create_block(index, data, previous_hash):
    """Creer un bloc avec lien vers le precedent via hash."""
    block = {
        "index": index,
        "timestamp": datetime.now().isoformat(),
        "data": data,
        "previous_hash": previous_hash,
    }
    block_string = json.dumps(block, sort_keys=True).encode()
    block["hash"] = hashlib.sha256(block_string).hexdigest()
    return block

# Construire une mini-blockchain
genesis = create_block(0, "Bloc Genesis", "0" * 64)
block1 = create_block(1, "Alice envoie 10 BTC a Bob", genesis["hash"])
block2 = create_block(2, "Bob envoie 5 BTC a Charlie", block1["hash"])
block3 = create_block(3, "Charlie envoie 2 BTC a Dave", block2["hash"])

chain = [genesis, block1, block2, block3]

print("MINI-BLOCKCHAIN (4 blocs)")
print("=" * 70)
for block in chain:
    print(f"Bloc {block['index']} : {block['data']}")
    print(f"  Hash     : {block['hash'][:32]}...")
    print(f"  Prev     : {block['previous_hash'][:32]}...")
    print()
MINI-BLOCKCHAIN (4 blocs)
======================================================================
Bloc 0 : Bloc Genesis
  Hash     : 772a18cdb54c4f7ed863e5a6eb30136e...
  Prev     : 00000000000000000000000000000000...

Bloc 1 : Alice envoie 10 BTC a Bob
  Hash     : f588d68b5d22b8bee5606f4bd3fa7137...
  Prev     : 772a18cdb54c4f7ed863e5a6eb30136e...

Bloc 2 : Bob envoie 5 BTC a Charlie
  Hash     : 3cebe538b85ec801d62ccc617fc46fae...
  Prev     : f588d68b5d22b8bee5606f4bd3fa7137...

Bloc 3 : Charlie envoie 2 BTC a Dave
  Hash     : ba627c14989df6735a38b41aedbe1880...
  Prev     : 3cebe538b85ec801d62ccc617fc46fae...

Détection de falsification dans la chaîne de blocs en vérifiant l’intégrité des hash successifs.

# Detection de falsification
print("DETECTION DE FALSIFICATION")
print("=" * 70)

original_data = chain[1]["data"]
original_hash = chain[1]["hash"]

# Modifier le bloc 1 (falsification)
chain[1]["data"] = "Alice envoie 1000 BTC a Bob"  # Fraude !
block_string = json.dumps(
    {k: v for k, v in chain[1].items() if k != "hash"}, sort_keys=True
).encode()
new_hash = hashlib.sha256(block_string).hexdigest()

print(f"Bloc 1 original : '{original_data}'")
print(f"Bloc 1 modifie  : '{chain[1]['data']}'")
print()
print(f"Hash original   : {original_hash[:40]}...")
print(f"Hash recalcule  : {new_hash[:40]}...")
print()

# Verification de la chaine
print("VERIFICATION DE LA CHAINE :")
chain[1]["hash"] = new_hash
for i in range(1, len(chain)):
    expected_prev = chain[i-1]["hash"]
    actual_prev = chain[i]["previous_hash"]
    valid = expected_prev == actual_prev
    status = "OK" if valid else "INVALIDE"
    print(f"  Bloc {i} -> previous_hash {status}")
    if not valid:
        print(f"    Attendu : {expected_prev[:32]}...")
        print(f"    Trouve  : {actual_prev[:32]}...")

# Restaurer
chain[1]["data"] = original_data
chain[1]["hash"] = original_hash
print()
print("-> Le bloc 2 pointe vers l'ancien hash du bloc 1 : la fraude est detectee !")
print("-> Pour falsifier, il faudrait recalculer TOUS les blocs suivants.")
DETECTION DE FALSIFICATION
======================================================================
Bloc 1 original : 'Alice envoie 10 BTC a Bob'
Bloc 1 modifie  : 'Alice envoie 1000 BTC a Bob'

Hash original   : f588d68b5d22b8bee5606f4bd3fa71375d32e170...
Hash recalcule  : b48db6c35a2f0e5f35e26a9f63f77d9971bb831d...

VERIFICATION DE LA CHAINE :
  Bloc 1 -> previous_hash OK
  Bloc 2 -> previous_hash INVALIDE
    Attendu : b48db6c35a2f0e5f35e26a9f63f77d99...
    Trouve  : f588d68b5d22b8bee5606f4bd3fa7137...
  Bloc 3 -> previous_hash OK

-> Le bloc 2 pointe vers l'ancien hash du bloc 1 : la fraude est detectee !
-> Pour falsifier, il faudrait recalculer TOUS les blocs suivants.

Interpretation — l’immuabilite par chainage, et sa limite

La chaîne de hash créé une structure de données immuable : modifier un bloc ancien casse tous les liens subsequents. C’est le fondement de l’integrite blockchain.

Mais cela ne suffit pas : un attaquant pourrait recalculer toute la chaîne. C’est la que la Proof-of-Work intervient (section 5) : rendre le recalcul prohibitivement couteux en temps et en energie.


4. Arbre de Merkle

Un arbre de Merkle (Ralph Merkle, 1979) permet de verifier l’integrite de N éléments avec seulement O(log N) hash. Il est utilise dans :

  • Bitcoin : verifier qu’une transaction est dans un bloc (SPV)
  • BitTorrent : verifier l’integrite des pieces telechargees
  • Git : la structure interne des commits
  • Ethereum : Patricia Merkle Trie pour l’etat des comptes

Principe

        Root Hash
       /         \\
    H(AB)       H(CD)
    /   \\       /   \\
  H(A)  H(B)  H(C)  H(D)
   |     |     |     |
  Tx A  Tx B  Tx C  Tx D

Pour prouver que Tx B est dans l’arbre, il suffit de fournir : H(A) + H(CD) + Root.

import hashlib

def sha256(data):
    """Hash SHA-256 d'une chaine."""
    if isinstance(data, str):
        data = data.encode()
    return hashlib.sha256(data).hexdigest()

def merkle_tree(transactions):
    """Construire un arbre de Merkle complet et retourner tous les niveaux."""
    if not transactions:
        return []
    current_level = [sha256(tx) for tx in transactions]
    tree = [current_level[:]]

    while len(current_level) > 1:
        next_level = []
        for i in range(0, len(current_level), 2):
            left = current_level[i]
            right = current_level[i + 1] if i + 1 < len(current_level) else left
            parent = sha256(left + right)
            next_level.append(parent)
        current_level = next_level
        tree.append(current_level[:])
    return tree

# Transactions exemple (comme dans un bloc Bitcoin)
transactions = [
    "Alice -> Bob: 1 BTC",
    "Bob -> Charlie: 0.5 BTC",
    "Dave -> Eve: 2 BTC",
    "Eve -> Frank: 0.3 BTC",
]

tree = merkle_tree(transactions)

print("ARBRE DE MERKLE")
print("=" * 70)
for level_idx, level in enumerate(tree):
    if level_idx == 0:
        label = "Feuilles"
    elif level_idx == len(tree) - 1:
        label = "Racine"
    else:
        label = f"Niveau {level_idx}"

    print(f"\n{label} ({len(level)} noeud(s)) :")
    for j, h in enumerate(level):
        if level_idx == 0:
            print(f"  [{j}] {h[:16]}... <- '{transactions[j]}'")
        else:
            print(f"  [{j}] {h[:16]}...")

print(f"\nMerkle Root : {tree[-1][0]}")
print(f"-> 4 transactions resumees en 1 seul hash de 256 bits")
ARBRE DE MERKLE
======================================================================

Feuilles (4 noeud(s)) :
  [0] 7d647288b6736b19... <- 'Alice -> Bob: 1 BTC'
  [1] b402cd80223159c9... <- 'Bob -> Charlie: 0.5 BTC'
  [2] 76b50d083bbe5124... <- 'Dave -> Eve: 2 BTC'
  [3] c9f9c448a2d7282f... <- 'Eve -> Frank: 0.3 BTC'

Niveau 1 (2 noeud(s)) :
  [0] 20e67cf979e88cda...
  [1] 613dc66e12a2097f...

Racine (1 noeud(s)) :
  [0] 8d1a98764d1baf07...

Merkle Root : 8d1a98764d1baf079ed3bf427789bbf1e9454a51c9c2be546cb487ec1c9ad78e
-> 4 transactions resumees en 1 seul hash de 256 bits

Génération d’une preuve Merkle permettant de vérifier qu’une transaction appartient à un bloc sans télécharger toute la blockchain.

def merkle_proof(transactions, tx_index):
    """Generer la preuve Merkle pour une transaction donnee."""
    tree = merkle_tree(transactions)
    proof = []
    idx = tx_index
    for level in tree[:-1]:
        if idx % 2 == 0:
            sibling_idx = idx + 1 if idx + 1 < len(level) else idx
            proof.append(("right", level[sibling_idx]))
        else:
            proof.append(("left", level[idx - 1]))
        idx //= 2
    return proof

def verify_proof(tx, proof, expected_root):
    """Verifier une preuve Merkle."""
    current = sha256(tx)
    for side, sibling in proof:
        if side == "right":
            current = sha256(current + sibling)
        else:
            current = sha256(sibling + current)
    return current == expected_root

# Generer et verifier la preuve pour la transaction 1
tx_index = 1
proof = merkle_proof(transactions, tx_index)
root = tree[-1][0]

print(f"PREUVE MERKLE pour transaction [{tx_index}]: '{transactions[tx_index]}'")
print("=" * 70)
print(f"Merkle Root attendue : {root[:32]}...")
print(f"\nPreuve ({len(proof)} elements, au lieu de {len(transactions)} transactions) :")
for side, h in proof:
    print(f"  {side:5s} : {h[:32]}...")

valid = verify_proof(transactions[tx_index], proof, root)
print(f"\nVerification : {'VALIDE' if valid else 'INVALIDE'}")
print(f"\n-> SPV (Bitcoin) : un noeud leger verifie une transaction")
print(f"   avec seulement {len(proof)} hash au lieu de {len(transactions)} transactions")
PREUVE MERKLE pour transaction [1]: 'Bob -> Charlie: 0.5 BTC'
======================================================================
Merkle Root attendue : 8d1a98764d1baf079ed3bf427789bbf1...

Preuve (2 elements, au lieu de 4 transactions) :
  left  : 7d647288b6736b19ab3bf3074249dea5...
  right : 613dc66e12a2097fd4a966c052140615...

Verification : VALIDE

-> SPV (Bitcoin) : un noeud leger verifie une transaction
   avec seulement 2 hash au lieu de 4 transactions

Interpretation — verification legere en O(log N)

L’arbre de Merkle permet la verification legere (SPV) : un telephone mobile peut verifier qu’une transaction est incluse dans un bloc Bitcoin sans telecharger les ~500 000 blocs complets (~500 Go).

Transactions dans le bloc Hash necessaires pour la preuve
1 000 10
1 000 000 20
1 000 000 000 30

La complexite logarithmique rend le système scalable a l’echelle mondiale.


5. Proof-of-Work (Hashcash)

Adam Back a invente Hashcash en 1997 comme système anti-spam pour les emails : pour envoyer un email, il faut trouver un nonce tel que le hash du message commence par N zeros. Cela coute quelques secondes de calcul a l’expediteur, mais rend l’envoi massif de spam economiquement impossible.

Satoshi Nakamoto a directement repris ce mécanisme pour le minage Bitcoin : - Le mineur cherche un nonce tel que SHA256(bloc + nonce) commence par N zeros - La difficulte N s’ajuste toutes les ~2 semaines pour maintenir ~10 min/bloc - La difficulte croit exponentiellement : chaque zero supplementaire double le travail moyen

import hashlib
import time

def proof_of_work(data, difficulty):
    """Trouver un nonce tel que hash(data+nonce) commence par N zeros."""
    target = "0" * difficulty
    nonce = 0
    start = time.time()
    while True:
        attempt = f"{data}{nonce}".encode()
        hash_val = hashlib.sha256(attempt).hexdigest()
        if hash_val[:difficulty] == target:
            elapsed = time.time() - start
            return nonce, hash_val, elapsed
        nonce += 1

data = "Bloc #42 - Alice envoie 1 BTC a Bob - prev_hash=abc123"

print("PROOF-OF-WORK (Hashcash / Bitcoin Mining)")
print("=" * 70)
print(f"Donnees : '{data[:50]}...'")
print()

for difficulty in range(1, 6):
    nonce, hash_val, elapsed = proof_of_work(data, difficulty)
    print(f"Difficulte {difficulty} (cible: {'0'*difficulty}{'x'*(8-difficulty)}) :")
    print(f"  Nonce    : {nonce:>10,}")
    print(f"  Hash     : {hash_val[:32]}...")
    print(f"  Temps    : {elapsed:.4f}s")
    print(f"  Essais   : ~{nonce:,} (theorique: ~{16**difficulty:,})")
    print()
PROOF-OF-WORK (Hashcash / Bitcoin Mining)
======================================================================
Donnees : 'Bloc #42 - Alice envoie 1 BTC a Bob - prev_hash=ab...'

Difficulte 1 (cible: 0xxxxxxx) :
  Nonce    :          6
  Hash     : 0c9aea00f1df55b854246da164d2cc73...
  Temps    : 0.0000s
  Essais   : ~6 (theorique: ~16)

Difficulte 2 (cible: 00xxxxxx) :
  Nonce    :        114
  Hash     : 003ecdb4d0fffd8a942d7ef196fbdba1...
  Temps    : 0.0001s
  Essais   : ~114 (theorique: ~256)

Difficulte 3 (cible: 000xxxxx) :
  Nonce    :        410
  Hash     : 000fcfcc32f345988c3f635964699b0b...
  Temps    : 0.0003s
  Essais   : ~410 (theorique: ~4,096)

Difficulte 4 (cible: 0000xxxx) :
  Nonce    :     39,430
  Hash     : 000034fef981e1dc59961bd97dcd6c69...
  Temps    : 0.0241s
  Essais   : ~39,430 (theorique: ~65,536)

Difficulte 5 (cible: 00000xxx) :
  Nonce    :    214,484
  Hash     : 00000bb794a36ce9ac55025613a1a499...
  Temps    : 0.1357s
  Essais   : ~214,484 (theorique: ~1,048,576)

Interpretation — la croissance exponentielle du travail

Difficulte Essais moyens Temps approximatif
1 ~16 instantane
2 ~256 instantane
3 ~4 096 quelques ms
4 ~65 536 ~0.05s
5 ~1 048 576 ~0.5s
6 ~16 millions ~10s

La croissance est exponentielle (x16 par niveau). Bitcoin ajuste la difficulte pour que le reseau mondial (~500 EH/s en 2025) mette ~10 minutes par bloc.

C’est ce mécanisme qui rend la falsification de la chaîne de hash (section 3) economiquement impossible : recalculer les blocs modifiés prendrait plus d’energie que l’ensemble du reseau honnete.

Exercice : Analyseur de difficulte PoW

Ecrivez une fonction analyze_pow(data, max_difficulty=6) qui teste le Proof-of-Work pour les niveaux de difficulte 1 a max_difficulty, puis retourne un dictionnaire avec le nombre d’essais et le temps pour chaque niveau. L’objectif est de verifier experimentalement la croissance exponentielle du travail.

Indices : - Reutilisez la fonction proof_of_work définie dans la section 5 - Pour chaque difficulte, stockez (nonce, temps) dans un dictionnaire résultats - Calculez le ratio d’essais entre difficulte N et N-1 pour confirmer le facteur ~16

# Exercice : Analyseur de difficulte PoW
def analyze_pow(data, max_difficulty=6):
    """Analyser la croissance exponentielle du Proof-of-Work.
    
    TODO etudiant : pour chaque difficulte de 1 a max_difficulty :
    - Appeler proof_of_work(data, difficulty)
    - Stocker le nonce et le temps dans un dictionnaire
    - Calculer le ratio d'essais avec le niveau precedent
    
    Retourner un dictionnaire {difficulty: {"nonce": ..., "time": ..., "ratio": ...}}
    """
    resultats = {}  # TODO etudiant : implementer
    return resultats

print("Exercice a completer")
Exercice a completer

6. Signatures numériques

Les signatures numériques sont l’equivalent electronique de la signature manuscrite, mais avec des garanties mathematiques :

  1. Authentification : seul le detenteur de la cle privee peut signer
  2. Integrite : toute modification du message invalide la signature
  3. Non-repudiation : le signataire ne peut nier avoir signe

Bitcoin et Ethereum utilisent des signatures sur courbes elliptiques (ECDSA/secp256k1). C’est le mécanisme qui lie une adresse (cle publique) a une transaction.

try:
    from Crypto.PublicKey import ECC
    from Crypto.Signature import DSS
    from Crypto.Hash import SHA256
except ImportError as e:
    print(f"Installation requise : pip install pycryptodome")
    print(f"Erreur : {e}")

print("SIGNATURES NUMERIQUES (Courbes Elliptiques)")
print("=" * 70)

# Generation de cles (equivalent d'un wallet crypto)
key = ECC.generate(curve='P-256')
public_key = key.public_key()

print(f"Cle privee (secret) : {hex(key.d)[:32]}...")
print(f"Cle publique X      : {hex(public_key.pointQ.x)[:32]}...")
print(f"Cle publique Y      : {hex(public_key.pointQ.y)[:32]}...")
print()

# Signer une transaction
transaction = b"Envoyer 1 ETH de 0xAlice a 0xBob"
h = SHA256.new(transaction)
signer = DSS.new(key, 'fips-186-3')
signature = signer.sign(h)

print(f"Transaction : {transaction.decode()}")
print(f"Signature   : {signature.hex()[:64]}...")
print(f"Taille      : {len(signature)} octets")
print()

# Verification (n'importe qui peut verifier avec la cle publique)
verifier = DSS.new(public_key, 'fips-186-3')
try:
    verifier.verify(SHA256.new(transaction), signature)
    print("Verification : VALIDE (la transaction est authentique)")
except ValueError:
    print("Verification : INVALIDE")

# Tentative de falsification
print()
fake_transaction = b"Envoyer 100 ETH de 0xAlice a 0xBob"
try:
    verifier.verify(SHA256.new(fake_transaction), signature)
    print("Falsification : ACCEPTEE (PROBLEME !)")
except ValueError:
    print("Falsification : REJETEE (la signature ne correspond pas)")
    print("-> Impossible de modifier la transaction sans la cle privee")
SIGNATURES NUMERIQUES (Courbes Elliptiques)
======================================================================
Cle privee (secret) : 0x8deb01a81e0662fbd021475baf12de...
Cle publique X      : 0xcb9422e58e193429df741a6b1c8d07...
Cle publique Y      : 0x7af6358ddbb94eb5613fabf03363e8...

Transaction : Envoyer 1 ETH de 0xAlice a 0xBob
Signature   : 1bafed5f9be62206eb091d4a91726bca3ff80698f7800479138fe53d0e115cc4...
Taille      : 64 octets

Verification : VALIDE (la transaction est authentique)

Falsification : REJETEE (la signature ne correspond pas)
-> Impossible de modifier la transaction sans la cle privee

Interpretation — de la cle privee a l’adresse

Le système de signatures ECDSA garantit que : - Seul Alice (detentrice de la cle privee) peut autoriser un transfert depuis son adresse - Modifier le montant ou le destinataire invalide la signature - N’importe quel noeud du reseau peut verifier la signature avec la cle publique

Adresse blockchain = hash de la cle publique

Cle privee (256 bits, secret)
    | multiplication sur courbe elliptique
Cle publique (512 bits, public)
    | SHA-256 + RIPEMD-160
Adresse (160 bits = 20 octets = "0x...")

C’est pourquoi perdre sa cle privee = perdre ses fonds, et pourquoi il ne faut jamais la partager.

Exercice : Vérification de signature (authenticité d’une transaction)

L’exemple ci-dessus délègue la vérification à verifier.verify(), qui lève une ValueError si la signature est invalide. En pratique, on préfère une fonction qui retourne un booléen (True = authentique, False = falsifiée) plutôt qu’une exception qu’il faut intercepter à chaque appel.

Objectif : implémenter verifier_signature(cle_publique, message, signature) qui renvoie True si la signature ECDSA est valide pour ce message et cette clé publique, False sinon.

Indice : encapsuler DSS.new(cle_publique, 'fips-186-3') .verify(SHA256.new(message), signature) dans un bloc try/except ValueError — verify lève ValueError quand la signature ne correspond pas au message.

Étape 1 : créer le vérifieur avec la clé publique et le mode 'fips-186-3'. Étape 2 : appeler .verify(...) dans un try. Étape 3 : retourner True si aucune exception n’est levée, False si une ValueError est interceptée.

# Exercice : Verification de signature (retourne un booleen, pas une exception)
# TODO etudiant : implementer verifier_signature qui retourne True/False.

from Crypto.Hash import SHA256
from Crypto.Signature import DSS

def verifier_signature(cle_publique, message, signature):
    """Vérifier l'authenticité d'un message signé.

    Retourner True si la signature ECDSA est valide pour
    (cle_publique, message), False sinon. Ne PAS lever d'exception.
    """
    # Etape 1 : creer le verifieur avec la cle publique et le mode 'fips-186-3'
    # Etape 2 : appeler .verify(SHA256.new(message), signature) dans un try
    # Etape 3 : retourner True si pas d'exception, False si ValueError
    return False  # TODO etudiant : implementer

print("Exercice a completer")
Exercice a completer

7. DHT et reseaux pair-a-pair

Le dernier ingredient est le reseau decentralise. Sans serveur central, comment les noeuds Bitcoin/Ethereum se trouvent-ils et echangent-ils des données ?

La reponse vient de BitTorrent et du protocole Kademlia (2002) : une table de hachage distribuee (DHT) ou chaque noeud stocke une partie des données, et la distance entre noeuds est mesuree par l’opération XOR sur leurs identifiants.

Ethereum utilise directement une variante de Kademlia pour la decouverte de pairs.

import hashlib

# Chaque noeud du reseau a un identifiant = hash de son adresse
nodes = {
    "noeud-paris:30303":   int(hashlib.sha256(b"noeud-paris:30303").hexdigest(), 16),
    "noeud-london:30303":  int(hashlib.sha256(b"noeud-london:30303").hexdigest(), 16),
    "noeud-tokyo:30303":   int(hashlib.sha256(b"noeud-tokyo:30303").hexdigest(), 16),
    "noeud-nyc:30303":     int(hashlib.sha256(b"noeud-nyc:30303").hexdigest(), 16),
}

print("TABLE DE HACHAGE DISTRIBUEE (DHT / Kademlia)")
print("=" * 70)

print("\nIdentifiants des noeuds (SHA-256 tronque a 16 hex) :")
for name, node_id in nodes.items():
    print(f"  {name:25s} -> {node_id & 0xFFFFFFFFFFFFFFFF:016x}")

# Distance XOR entre noeuds
print("\nDistances XOR (plus petit = plus proche) :")
names = list(nodes.keys())
ids = list(nodes.values())

for i in range(len(names)):
    for j in range(i + 1, len(names)):
        distance = ids[i] ^ ids[j]
        dist_bits = distance.bit_length()
        print(f"  {names[i]:15s} <-> {names[j]:15s} : {dist_bits} bits")

print()
print("-> Kademlia : chaque noeud connait ~log(N) autres noeuds")
print("-> Trouver une donnee prend ~log(N) sauts (routage iteratif)")
print("-> Pas de serveur central, resilient aux pannes et a la censure")
TABLE DE HACHAGE DISTRIBUEE (DHT / Kademlia)
======================================================================

Identifiants des noeuds (SHA-256 tronque a 16 hex) :
  noeud-paris:30303         -> 3dbba68547a2f49d
  noeud-london:30303        -> d8c971c6e2920124
  noeud-tokyo:30303         -> 5554c6f0b2c809db
  noeud-nyc:30303           -> bfa57edb7fb9d6f9

Distances XOR (plus petit = plus proche) :
  noeud-paris:30303 <-> noeud-london:30303 : 256 bits
  noeud-paris:30303 <-> noeud-tokyo:30303 : 256 bits
  noeud-paris:30303 <-> noeud-nyc:30303 : 255 bits
  noeud-london:30303 <-> noeud-tokyo:30303 : 254 bits
  noeud-london:30303 <-> noeud-nyc:30303 : 256 bits
  noeud-tokyo:30303 <-> noeud-nyc:30303 : 256 bits

-> Kademlia : chaque noeud connait ~log(N) autres noeuds
-> Trouver une donnee prend ~log(N) sauts (routage iteratif)
-> Pas de serveur central, resilient aux pannes et a la censure

Implémentation du format Bencode utilisé par BitTorrent pour sérialiser les métadonnées de torrents.

import hashlib

def bencode(data):
    """Encoder des donnees au format bencode (BitTorrent)."""
    if isinstance(data, int):
        return f"i{data}e".encode()
    elif isinstance(data, bytes):
        return f"{len(data)}:".encode() + data
    elif isinstance(data, str):
        data_bytes = data.encode()
        return f"{len(data_bytes)}:".encode() + data_bytes
    elif isinstance(data, list):
        return b"l" + b"".join(bencode(x) for x in data) + b"e"
    elif isinstance(data, dict):
        items = sorted(data.items())
        return b"d" + b"".join(
            bencode(k.encode() if isinstance(k, str) else k) + bencode(v)
            for k, v in items
        ) + b"e"
    raise TypeError(f"Type non supporte: {type(data)}")

# Simuler un fichier .torrent
torrent_info = {
    "name": "bitcoin-whitepaper.pdf",
    "length": 184292,
    "piece length": 262144,
    "pieces": b"\x00" * 20,
}

encoded = bencode(torrent_info)
info_hash = hashlib.sha1(encoded).hexdigest()

print("BENCODAGE BITTORRENT")
print("=" * 70)
print(f"Fichier     : {torrent_info['name']}")
print(f"Taille      : {torrent_info['length']:,} octets")
print(f"Bencode     : {encoded[:60]}...")
print(f"Info hash   : {info_hash}")
print()
print(f"-> L'info_hash est l'identifiant unique du torrent dans la DHT")
print(f"-> C'est le 'magnet link' : magnet:?xt=urn:btih:{info_hash[:20]}...")
BENCODAGE BITTORRENT
======================================================================
Fichier     : bitcoin-whitepaper.pdf
Taille      : 184,292 octets
Bencode     : b'd6:lengthi184292e4:name22:bitcoin-whitepaper.pdf12:piece len'...
Info hash   : 744ab5f5c309dcae224fb2c44f3591cd7ae2671f

-> L'info_hash est l'identifiant unique du torrent dans la DHT
-> C'est le 'magnet link' : magnet:?xt=urn:btih:744ab5f5c309dcae224f...

Interpretation — decentralisation contre serveur central

Les reseaux P2P apportent la decentralisation necessaire aux blockchains :

Propriete Serveur central P2P (DHT)
Point de defaillance unique Oui Non
Censurable Oui (saisie du serveur) Très difficile
Scalabilite Limitee O(log N) par noeud
Exemples Napster (1999, ferme) BitTorrent (2001, toujours actif)

Ethereum herite directement de Kademlia : le port par defaut (30303) et le protocole de decouverte sont bases sur la DHT.


8. Synthese : de Chaum a Ethereum

Voici la frise complete montrant comment chaque brique Cypherpunk a ete integree dans les blockchains modernes.

# Frise chronologique : briques fondatrices et leur heritage
timeline = [
    (1976, "Diffie-Hellman",     "Echange de cles",           ["Signature"], "CRYPTO"),
    (1977, "RSA",                "Chiffrement asymetrique",   ["Signature"], "CRYPTO"),
    (1979, "Merkle",             "Arbre de Merkle",           ["Bitcoin SPV", "Git", "BitTorrent"], "STRUCTURE"),
    (1985, "Chaum",              "eCash (monnaie anonyme)",   ["Zcash", "Monero"], "MONNAIE"),
    (1991, "PGP",                "Crypto grand public",       ["GPG", "Signal"], "CRYPTO"),
    (1991, "Haber-Stornetta",    "Horodatage par hash chain", ["Bitcoin blockchain"], "STRUCTURE"),
    (1997, "Hashcash",           "Proof-of-Work",             ["Bitcoin mining"], "CONSENSUS"),
    (1998, "b-money",            "Monnaie decentralisee",     ["Bitcoin"], "MONNAIE"),
    (2001, "BitTorrent",         "DHT Kademlia, P2P massif",  ["Ethereum P2P"], "RESEAU"),
    (2002, "Kademlia",           "DHT avec distance XOR",     ["Ethereum devp2p"], "RESEAU"),
    (2008, "Bitcoin",            "Synthese de tout",          ["Ethereum", "1000+ altcoins"], "BLOCKCHAIN"),
    (2013, "Ethereum",           "Smart Contracts (Turing)",  ["DeFi", "NFT", "DAO"], "BLOCKCHAIN"),
    (2015, "Ethereum mainnet",   "Lancement production",      ["ERC-20", "ERC-721", "DeFi 2020"], "BLOCKCHAIN"),
]

print("FRISE CHRONOLOGIQUE : CYPHERPUNKS -> SMART CONTRACTS")
print("=" * 70)

for year, name, description, heritage, cat in timeline:
    marker = {"CRYPTO": "[C]", "STRUCTURE": "[S]", "MONNAIE": "[M]",
              "CONSENSUS": "[W]", "RESEAU": "[R]", "BLOCKCHAIN": "[B]"}[cat]
    print(f"  {year} {marker} {name:20s} | {description}")
    if heritage:
        print(f"       {'':20s}   -> {', '.join(heritage)}")
    print()

print("Legende : [C]=Crypto [S]=Structure [M]=Monnaie [W]=Consensus [R]=Reseau [B]=Blockchain")
FRISE CHRONOLOGIQUE : CYPHERPUNKS -> SMART CONTRACTS
======================================================================
  1976 [C] Diffie-Hellman       | Echange de cles
                              -> Signature

  1977 [C] RSA                  | Chiffrement asymetrique
                              -> Signature

  1979 [S] Merkle               | Arbre de Merkle
                              -> Bitcoin SPV, Git, BitTorrent

  1985 [M] Chaum                | eCash (monnaie anonyme)
                              -> Zcash, Monero

  1991 [C] PGP                  | Crypto grand public
                              -> GPG, Signal

  1991 [S] Haber-Stornetta      | Horodatage par hash chain
                              -> Bitcoin blockchain

  1997 [W] Hashcash             | Proof-of-Work
                              -> Bitcoin mining

  1998 [M] b-money              | Monnaie decentralisee
                              -> Bitcoin

  2001 [R] BitTorrent           | DHT Kademlia, P2P massif
                              -> Ethereum P2P

  2002 [R] Kademlia             | DHT avec distance XOR
                              -> Ethereum devp2p

  2008 [B] Bitcoin              | Synthese de tout
                              -> Ethereum, 1000+ altcoins

  2013 [B] Ethereum             | Smart Contracts (Turing)
                              -> DeFi, NFT, DAO

  2015 [B] Ethereum mainnet     | Lancement production
                              -> ERC-20, ERC-721, DeFi 2020

Legende : [C]=Crypto [S]=Structure [M]=Monnaie [W]=Consensus [R]=Reseau [B]=Blockchain

Interpretation — trois phases dans la genealogie cypherpunk

La frise revele trois phases distinctes dans la genealogie cypherpunk :

Phase Periode Innovations Heritage
Fondations crypto 1976-1991 Diffie-Hellman, RSA, Merkle, PGP Signatures, integrite
Decentralisation 1991-2002 Hashcash, b-money, BitTorrent, Kademlia Consensus, P2P
Synthese 2008-2015 Bitcoin, Ethereum Blockchain, smart contracts

Points cles de la frise : - Bitcoin n’a invente aucune primitive : son apport est l’agencement de 7 briques preexistantes - Chaque catégorie [C]rypto, [S]tructure, [M]onnaie, [W]consensus, [R]eseau est indispensable — en retirer une casse le système - Ethereum ajoute la Turing-completude (smart contracts) sur cette base, transformant un système de paiement en ordinateur decentralise mondial


9. Exercice : Construire une mini-blockchain complete

En combinant toutes les briques de ce notebook, implementez une mini-blockchain fonctionnelle.

# Exercice : Mini-blockchain complete
# Combiner : hash chain + Merkle tree + PoW + signatures

import hashlib
import json
import time

try:
    from Crypto.PublicKey import ECC
    from Crypto.Signature import DSS
    from Crypto.Hash import SHA256
except ImportError as e:
    print(f"Installation requise : pip install pycryptodome")
    print(f"Erreur : {e}")


class MiniBlockchain:
    """Mini-blockchain combinant toutes les primitives Cypherpunk."""

    def __init__(self, difficulty=3):
        self.chain = []
        self.difficulty = difficulty
        self.pending_transactions = []

    def create_genesis_block(self):
        """Creer le bloc genesis.
        TODO: Creer un bloc avec index=0, transactions=[], previous_hash="0"*64
        et le miner (trouver un nonce valide).
        """
        pass  # TODO: Implementez le bloc genesis
print("Exercice a completer")
Exercice a completer

10. Resume

Primitive Inventeur Annee Rôle dans la blockchain
Hash (SHA-256) NSA 2001 Integrite, adresses, PoW
Chaîne de hash Haber-Stornetta 1991 Immuabilite des blocs
Arbre de Merkle Ralph Merkle 1979 Verification legere (SPV)
Proof-of-Work Adam Back 1997 Consensus, anti-falsification
Signatures ECDSA Johnson et al. 2001 Authentification des transactions
DHT Kademlia Maymounkov-Mazieres 2002 Reseau decentralise P2P

Points cles

  • Les smart contracts sont l’heritage direct du mouvement Cypherpunk des annees 1990
  • Chaque brique (hash, signatures, PoW, P2P) existait avant Bitcoin : l’apport de Satoshi Nakamoto est leur combinaison en un système coherent
  • La frise de la section 8 revele trois phases distinctes : fondations cryptographiques (1976-1991), decentralisation (1991-2002), puis synthese blockchain (2008-2015)
  • Chaque compromis technique (scalabilite vs decentralisation, confidentialite vs transparence) trouve son origine dans ces choix architecturaux fondamentaux
  • Comprendre ces fondations est essentiel pour evaluer la securite et les limites des blockchains modernes

Ces bases historiques et techniques posées, le notebook suivant SC-01-Setup-Foundry-Python passe à la pratique en installant l’environnement de développement Foundry (forge, cast, anvil) qui sera utilisé tout au long de la série SmartContracts.


Notebook suivant : SC-01-Setup-Foundry-Python - Installation de l’environnement de développement Foundry (forge, cast, anvil), utilisé tout au long de la serie SmartContracts


<< Retour au sommaire | Suivant : Setup Foundry >>

Retour au sommet