Comprendre le modèle UTXO de Bitcoin et ses différences avec le modèle Account d’Ethereum
Decouvrir Bitcoin Script, le langage a pile de Bitcoin
Implementer un mini-interpreteur Bitcoin Script en Python
Construire et signer des transactions Bitcoin manuellement
Explorer les scripts avances : multisig, timelock, P2SH
Prerequis
Python 3.10+ avec hashlib (stdlib)
python-bitcoinlib (optionnel, pour la section 3)
Duree estimee : 50 minutes
1. Le modèle UTXO
Bitcoin utilise un modèle fondamentalement différent d’Ethereum pour representer les soldes. Au lieu d’un modèle Account (chaque adresse a un solde), Bitcoin utilise le modèle UTXO (Unspent Transaction Output).
Account vs UTXO
Aspect
Account (Ethereum)
UTXO (Bitcoin)
Representation
Solde global par adresse
Ensemble de “pieces” non depensees
Analogie
Compte bancaire
Pieces et billets dans un portefeuille
Transaction
Debiter un compte, crediter un autre
Consommer des UTXO, en créer de nouveaux
Verification
Verifier le solde >= montant
Verifier que les UTXO existent et ne sont pas depenses
Parallelisme
Difficile (etat global)
Naturel (UTXO independants)
Confidentialite
Solde visible
Montants repartis en UTXO multiples
Principe
Un UTXO est une sortie de transaction non depensee. Chaque transaction : 1. Consomme un ou plusieurs UTXO existants (les inputs) 2. Créé de nouveaux UTXO (les outputs) 3. La somme des inputs >= somme des outputs (la différence = frais pour le mineur)
C’est comme payer avec des billets : si vous avez un billet de 50 et devez payer 30, vous donnez le billet de 50 et recevez 20 de monnaie.
import hashlibimport jsonfrom collections import defaultdictclass UTXOSet:"""Gestionnaire d'UTXO simplifie modelisant le coeur de Bitcoin."""def__init__(self):# UTXO = {(tx_hash, output_index): {"address": ..., "amount": ...}}self.utxos = {}self.tx_history = []def _tx_hash(self, tx_data):"""Calculer le hash d'une transaction."""return hashlib.sha256(json.dumps(tx_data, sort_keys=True).encode()).hexdigest()def add_coinbase(self, address, amount, label="coinbase"):"""Creer un UTXO de generation (recompense de minage).""" tx_data = {"type": "coinbase", "to": address, "amount": amount, "label": label} tx_hash =self._tx_hash(tx_data)self.utxos[(tx_hash, 0)] = {"address": address, "amount": amount}self.tx_history.append(tx_data)return tx_hashdef get_balance(self, address):"""Calculer le solde d'une adresse en sommant ses UTXO."""returnsum( utxo["amount"] for utxo inself.utxos.values()if utxo["address"] == address )def get_utxos_for(self, address):"""Lister les UTXO d'une adresse."""return { key: utxo for key, utxo inself.utxos.items()if utxo["address"] == address }def create_transaction(self, sender, recipient, amount, fee=0):"""Creer une transaction UTXO. 1. Selectionner des UTXO suffisants (coin selection) 2. Consommer ces UTXO (inputs) 3. Creer de nouveaux UTXO (outputs) : recipient + change """ sender_utxos =self.get_utxos_for(sender) total_available =sum(u["amount"] for u in sender_utxos.values())if total_available < amount + fee:raiseValueError(f"Solde insuffisant: {total_available} < {amount + fee}" )# Coin selection (simple: on prend les UTXO un par un) selected = {} selected_total =0for key, utxo in sender_utxos.items(): selected[key] = utxo selected_total += utxo["amount"]if selected_total >= amount + fee:break# Consommer les UTXO selectionnesfor key in selected:delself.utxos[key]# Creer les outputs tx_data = {"inputs": [{"tx": k[0][:16], "index": k[1]} for k in selected],"outputs": [{"address": recipient, "amount": amount}],"fee": fee, } change = selected_total - amount - feeif change >0: tx_data["outputs"].append({"address": sender, "amount": change}) tx_hash =self._tx_hash(tx_data)# Ajouter les nouveaux UTXOfor i, output inenumerate(tx_data["outputs"]):self.utxos[(tx_hash, i)] = {"address": output["address"],"amount": output["amount"], }self.tx_history.append(tx_data)return tx_hash, tx_data# Demonstrationutxo_set = UTXOSet()# Minage initial : Alice recoit 50 BTC (coinbase)print("MODELE UTXO - Demonstration")print("="*60)tx0 = utxo_set.add_coinbase("Alice", 50, "bloc-0-coinbase")print(f"Coinbase -> Alice : 50 BTC (tx: {tx0[:16]}...)")tx1 = utxo_set.add_coinbase("Alice", 25, "bloc-1-coinbase")print(f"Coinbase -> Alice : 25 BTC (tx: {tx1[:16]}...)")print(f"\nSolde Alice : {utxo_set.get_balance('Alice')} BTC")print(f"UTXO d'Alice : {len(utxo_set.get_utxos_for('Alice'))} pieces")
Le modèle UTXO (Unspent Transaction Output) est fondamentalement différent du modèle de comptes d’Ethereum.
Aspect
UTXO (Bitcoin)
Comptes (Ethereum)
Etat
Ensemble d’outputs non depenses
Solde par adresse
Transaction
Consomme N UTXO, produit M UTXO
Decremente/Augmente des soldes
Parallelisme
Naturel (UTXO independants)
Séquentiel (solde partage)
Double-depense
Output déjà consomme = invalide
Solde insuffisant = invalide
Privacy
Nouvelle adresse par UTXO
Adresse persistante
Points cles : - Chaque UTXO est indestructible : il est soit non depense, soit consomme par exactement une transaction - La transaction “coinbase” (minage) est le seul mécanisme de creation de nouveaux UTXO — l’offre monetaire augmente uniquement par le minage - Le txid:vout (hash de transaction + index d’output) est l’identifiant unique d’un UTXO - Les frais de transaction = somme(inputs) - somme(outputs) — le mineur collecte la différence
Simulation d’une transaction UTXO d’Alice vers Bob pour illustrer le mécanisme de dépense des sorties non dépensées.
# Transaction : Alice envoie 30 BTC a Bobprint("TRANSACTION : Alice -> Bob (30 BTC, frais 1 BTC)")print("="*60)tx_hash, tx_data = utxo_set.create_transaction("Alice", "Bob", 30, fee=1)print(f"\nTx hash : {tx_hash[:32]}...")print(f"Inputs : {len(tx_data['inputs'])} UTXO consomme(s)")for inp in tx_data["inputs"]:print(f" <- tx {inp['tx']}... index {inp['index']}")print(f"Outputs : {len(tx_data['outputs'])} UTXO cree(s)")for out in tx_data["outputs"]:print(f" -> {out['address']} : {out['amount']} BTC")print(f"Frais : {tx_data['fee']} BTC (pour le mineur)")print(f"\nApres transaction :")print(f" Alice : {utxo_set.get_balance('Alice')} BTC ({len(utxo_set.get_utxos_for('Alice'))} UTXO)")print(f" Bob : {utxo_set.get_balance('Bob')} BTC ({len(utxo_set.get_utxos_for('Bob'))} UTXO)")# Bob envoie 15 BTC a Charlieprint(f"\nTRANSACTION : Bob -> Charlie (15 BTC, frais 0.5 BTC)")tx_hash2, tx_data2 = utxo_set.create_transaction("Bob", "Charlie", 15, fee=0.5)print(f"\nEtat final du UTXO set :")for addr in ["Alice", "Bob", "Charlie"]: utxos = utxo_set.get_utxos_for(addr)print(f" {addr:10s}: {utxo_set.get_balance(addr):>7.1f} BTC ({len(utxos)} UTXO)")print(f"\nTotal UTXO dans le systeme : {len(utxo_set.utxos)}")print("-> Chaque UTXO est une 'piece' independante, prete a etre depensee")
TRANSACTION : Alice -> Bob (30 BTC, frais 1 BTC)
============================================================
Tx hash : 5b587d17ea3580919f0dae86e2951459...
Inputs : 1 UTXO consomme(s)
<- tx 2a07f237163da93c... index 0
Outputs : 2 UTXO cree(s)
-> Bob : 30 BTC
-> Alice : 19 BTC
Frais : 1 BTC (pour le mineur)
Apres transaction :
Alice : 44 BTC (2 UTXO)
Bob : 30 BTC (1 UTXO)
TRANSACTION : Bob -> Charlie (15 BTC, frais 0.5 BTC)
Etat final du UTXO set :
Alice : 44.0 BTC (2 UTXO)
Bob : 14.5 BTC (1 UTXO)
Charlie : 15.0 BTC (1 UTXO)
Total UTXO dans le systeme : 4
-> Chaque UTXO est une 'piece' independante, prete a etre depensee
Interpretation : Modèle UTXO
Points cles observes :
Alice avait 2 UTXO (50 + 25 = 75 BTC). Pour envoyer 30 BTC, le système a consomme un UTXO de 50 et créé 2 nouveaux UTXO : 30 pour Bob et 19 de monnaie pour Alice.
Les frais (1 BTC) sont la différence entre inputs et outputs : ils vont implicitement au mineur.
L’UTXO de 25 BTC d’Alice reste intact : il n’a pas ete utilise.
Propriete
Consequence
UTXO atomiques
Impossible de depenser “une partie” d’un UTXO
Change explicite
Toute différence doit etre renvoyee comme monnaie
Indépendance
Deux transactions avec des UTXO différents sont parallelisables
Tracabilite
On peut remonter la chaîne de propriete de chaque UTXO
1b. Exercice : Analyseur de flux de transactions UTXO
Objectif
Implementez une fonction trace_utxo_flow qui suit le cheminement d’un UTXO a travers plusieurs transactions. Etant donne une adresse cible, la fonction doit retracer l’historique des transactions pour determiner la source originelle des fonds.
Contexte
L’analyse de flux (chain analysis) est essentielle en investigation blockchain. Chaque UTXO peut etre trace jusqu’a la transaction coinbase (minage) qui l’a créé. La fonction doit parcourir l’historique et afficher la chaîne de propriete.
Indice :
Utilisez la classe UTXOSet et son attribut tx_history
Pour chaque transaction, identifiez les inputs et outputs concernes
Construisez une liste representant le chemin : chaque étape contient l’adresse source, l’adresse destination et le montant
La chaîne s’arrete quand une transaction coinbase est trouvee (source = minage)
def trace_utxo_flow(utxo_set, target_address):"""Tracer le flux de fonds vers une adresse cible.TODO etudiant : parcourez l'historique des transactions (utxo_set.tx_history) et reconstruisez la chaine de propriete des fonds recus par target_address. Pour chaque transaction dans l'historique : 1. Identifiez si target_address recoit des fonds (output) 2. Identifiez l'adresse source (input ou coinbase) 3. Recursez pour tracer la source jusqu'a la coinbase Args: utxo_set: instance de UTXOSet avec tx_history rempli target_address: adresse dont on veut tracer les fonds Returns: liste de dictionnaires, chacun avec 'from', 'to', 'amount' """# TODO etudiant : implementez le traceurreturn []# Creation d'un scenario de testtrace_test = UTXOSet()trace_test.add_coinbase("Miner", 50, "coinbase-bloc-0")trace_test.create_transaction("Miner", "Exchange", 20, fee=0.5)trace_test.create_transaction("Exchange", "Alice", 10, fee=0.1)trace_test.create_transaction("Alice", "Bob", 5, fee=0.05)print("Exercice a completer : implementez trace_utxo_flow")print(" Scenario de test cree :")print(f" Miner -> Exchange -> Alice -> Bob")print(f" Transactions : {len(trace_test.tx_history)}")print(f" Tracez les fonds de 'Bob' jusqu'a la coinbase")
Exercice a completer : implementez trace_utxo_flow
Scenario de test cree :
Miner -> Exchange -> Alice -> Bob
Transactions : 4
Tracez les fonds de 'Bob' jusqu'a la coinbase
2. Bitcoin Script
Bitcoin Script est un langage a pile (stack-based) volontairement limite : il est Turing-incomplet par conception, pour eviter les boucles infinies et garantir que la verification se termine toujours.
Principe
Chaque UTXO contient un script de verrouillage (scriptPubKey) qui définit les conditions pour depenser cet UTXO. Pour le depenser, l’emetteur fournit un script de deverrouillage (scriptSig) qui satisfait ces conditions.
scriptSig + scriptPubKey -> exécution sur la pile -> résultat TRUE/FALSE
Opcodes principaux
Opcode
Effet sur la pile
Description
OP_DUP
a -> a a
Duplique le sommet de la pile
OP_HASH160
a -> HASH160(a)
RIPEMD160(SHA256(a))
OP_EQUAL
a b -> (a==b)
Egalite, pousse TRUE/FALSE
OP_EQUALVERIFY
a b -> (vide si OK)
Egalite + echec si FALSE
OP_CHECKSIG
sig pubkey -> TRUE/FALSE
Verifie une signature ECDSA
OP_CHECKMULTISIG
… -> TRUE/FALSE
Verifie M signatures sur N cles
OP_IF / OP_ELSE / OP_ENDIF
Conditionnel
Branchement selon le sommet
OP_RETURN
-
Marque l’output comme non-depensable
OP_CHECKLOCKTIMEVERIFY
n -> n
Verifie que le timelock est atteint
import hashlibdef hash160(data):"""HASH160 = RIPEMD160(SHA256(data)), utilise par Bitcoin pour les adresses.""" sha = hashlib.sha256(data).digest() ripemd = hashlib.new("ripemd160", sha).digest()return ripemdclass BitcoinScriptInterpreter:"""Mini-interpreteur Bitcoin Script en Python. Supporte un sous-ensemble des opcodes reels. """def__init__(self):self.stack = []self.opcodes = {"OP_DUP": self._op_dup,"OP_HASH160": self._op_hash160,"OP_EQUAL": self._op_equal,"OP_EQUALVERIFY": self._op_equalverify,"OP_VERIFY": self._op_verify,"OP_CHECKSIG": self._op_checksig,"OP_ADD": self._op_add,"OP_SUB": self._op_sub,"OP_TRUE": self._op_true,"OP_DROP": self._op_drop,"OP_2DUP": self._op_2dup,"OP_SWAP": self._op_swap, }def _op_dup(self):"""Duplique le sommet de la pile."""iflen(self.stack) <1:raiseRuntimeError("Pile vide pour OP_DUP")self.stack.append(self.stack[-1])def _op_hash160(self):"""RIPEMD160(SHA256(top)).""" val =self.stack.pop()ifisinstance(val, str): val = val.encode()self.stack.append(hash160(val))def _op_equal(self):"""Compare les deux elements du sommet.""" b =self.stack.pop() a =self.stack.pop()self.stack.append(a == b)def _op_equalverify(self):"""EQUAL + VERIFY : echoue si pas egaux."""self._op_equal()ifnotself.stack.pop():raiseRuntimeError("OP_EQUALVERIFY echoue")def _op_verify(self):"""Echoue si le sommet est FALSE."""ifnotself.stack.pop():raiseRuntimeError("OP_VERIFY echoue")def _op_checksig(self):"""Verification simplifiee de signature (simulee).""" pubkey =self.stack.pop() signature =self.stack.pop()# Simulation : la signature est valide si elle contient# le hash de la cle publique (simplifie pour la pedagogie)ifisinstance(pubkey, str): pubkey = pubkey.encode() expected_sig = hashlib.sha256(pubkey).hexdigest()[:16] is_valid = (signature == expected_sig)self.stack.append(is_valid)def _op_add(self): b =self.stack.pop() a =self.stack.pop()self.stack.append(a + b)def _op_sub(self): b =self.stack.pop() a =self.stack.pop()self.stack.append(a - b)def _op_true(self):self.stack.append(True)def _op_drop(self):self.stack.pop()def _op_2dup(self):iflen(self.stack) <2:raiseRuntimeError("Pile insuffisante pour OP_2DUP")self.stack.append(self.stack[-2])self.stack.append(self.stack[-2])def _op_swap(self):self.stack[-1], self.stack[-2] =self.stack[-2], self.stack[-1]def execute(self, script, verbose=True):"""Executer un script Bitcoin (liste d'opcodes et donnees)."""self.stack = []if verbose:print(f"Script : {script}")print("-"*50)for item in script:if item inself.opcodes:if verbose:print(f" {item:20s}", end="")self.opcodes[item]()else:if verbose: display = item ifnotisinstance(item, bytes) else item.hex()[:16] +"..."print(f" PUSH {str(display):14s}", end="")self.stack.append(item)if verbose: stack_display = []for s inself.stack:ifisinstance(s, bytes): stack_display.append(s.hex()[:12] +"...")else: stack_display.append(str(s))print(f" pile: {stack_display}")returnlen(self.stack) >0andself.stack[-1]# Demonstration : script arithmetique simpleinterp = BitcoinScriptInterpreter()print("MINI-INTERPRETEUR BITCOIN SCRIPT")print("="*60)print()print("--- Script 1 : Verification arithmetique (2 + 3 == 5) ---")result = interp.execute([2, 3, "OP_ADD", 5, "OP_EQUAL"])print(f"Resultat : {result}")print()
Interpretation : Trace d’exécution du mini-interpreteur
L’exécution d’un script Bitcoin suit un modèle pile (stack) strictement déterministe, sans boucle ni saut.
Étape
Opération
Pile (haut a gauche)
Description
1
PUSH 2
2
Empile la valeur 2
2
PUSH 3
3, 2
Empile la valeur 3
3
OP_ADD
5
Depile 2 éléments, empile la somme
4
PUSH 5
5, 5
Empile la valeur attendue
5
OP_EQUAL
True
Verifie l’egalite
Points cles : - Le script s’arrete des qu’une verification echoue (OP_EQUALVERIFY, OP_CHECKSIG) — pas de gestion d’erreur, juste un rejet - Pas de boucles (pas de OP_JUMP) : c’est le design “non-Turing-complet” de Bitcoin, garantissant que l’exécution termine toujours - L’interpreteur ci-dessus simplifie les opérations reelles mais capture la logique fondamentale P2PKH - Chaque opcode consomme un nombre fixe d’éléments de pile — le cout en gaz est donc previsible (contrairement a Ethereum)
Implémentation du script P2PKH (Pay-to-Public-Key-Hash), le type de script de verrouillage le plus courant dans Bitcoin.
# P2PKH : Pay-to-Public-Key-Hash# Le script de verrouillage le plus courant de Bitcoin# ~95% des transactions Bitcoin utilisent ce formatprint("--- Script P2PKH (Pay-to-Public-Key-Hash) ---")print()# Simuler une cle publique et son hashpubkey ="02abc123def456...alice_public_key"pubkey_hash = hash160(pubkey.encode())# La signature simulee (en realite : ECDSA sur la transaction)signature = hashlib.sha256(pubkey.encode()).hexdigest()[:16]print(f"Cle publique : {pubkey[:30]}...")print(f"HASH160(pubkey): {pubkey_hash.hex()}")print(f"Signature : {signature}")print()# Script complet P2PKH :# scriptSig (deverrouillage) : <signature> <pubkey># scriptPubKey (verrouillage) : OP_DUP OP_HASH160 <pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG# En Bitcoin, on concatene scriptSig + scriptPubKeyfull_script = [# --- scriptSig (fourni par celui qui depense) --- signature, # signature ECDSA pubkey, # cle publique# --- scriptPubKey (inscrit dans l'UTXO) ---"OP_DUP", # dupliquer la cle publique"OP_HASH160", # hasher la copie pubkey_hash, # hash attendu (stocke dans l'UTXO)"OP_EQUALVERIFY", # verifier que les hash correspondent"OP_CHECKSIG", # verifier la signature]print("Execution du script P2PKH :")print("(scriptSig + scriptPubKey concatenes)")print()interp2 = BitcoinScriptInterpreter()result = interp2.execute(full_script)print(f"\nResultat : {result}")print("-> La transaction est autorisee : le detenteur de la cle privee peut depenser l'UTXO")
Le script P2PKH execute les étapes suivantes sur la pile :
Étape
Opération
Pile (sommet a droite)
1
PUSH signature
[sig]
2
PUSH pubkey
[sig, pubkey]
3
OP_DUP
[sig, pubkey, pubkey]
4
OP_HASH160
[sig, pubkey, hash(pubkey)]
5
PUSH expected_hash
[sig, pubkey, hash(pubkey), expected_hash]
6
OP_EQUALVERIFY
[sig, pubkey] (echoue si hash différent)
7
OP_CHECKSIG
[TRUE] (signature valide)
Pourquoi Turing-incomplet ? - Pas de boucles -> exécution garantie en temps fini - Pas de memoire persistante -> pas d’etat entre transactions - Securite maximale : on ne peut pas créer de contrat qui boucle indefiniment - Contrepartie : impossible d’ecrire des contrats complexes comme sur Ethereum
2b. Exercice : Script de depense conditionnelle
Objectif
Construisez un script Bitcoin Script qui implemente une condition de depense basique : un secret (preimage) doit etre revele pour depenser l’UTXO.
Ce mécanisme est la base des hash locks utilises dans les Atomic Swaps et le Lightning Network.
Principe
Le script verrouille les fonds avec un hash. Pour depenser, il faut fournir la valeur originale (preimage) dont le hash correspond. C’est le pattern “preimage reveal” :
Utilisez l’interpreteur BitcoinScriptInterpreter déjà défini
Generez un preimage (secret), calculez son hash avec la fonction hash160()
Construisez le script de verrouillage et le script de deverouillage
Executez le script combine avec l’interpreteur pour verifier qu’il passe
def build_hashlock_script(secret_preimage):"""Construire un script Bitcoin de hash lock (preimage reveal).TODO etudiant : 1. Calculez le hash du preimage avec hash160() 2. Construisez le script complet (scriptSig + scriptPubKey concatene) : [<preimage>, OP_HASH160, <expected_hash>, OP_EQUALVERIFY] 3. Executez-le avec l'interpreteur BitcoinScriptInterpreter Args: secret_preimage: le secret (bytes ou str) a utiliser comme preimage Returns: tuple: (script, result, expected_hash_hex) """# TODO etudiant : implementez le script script = [] # TODO : construisez le script result =False# TODO : executez avec l'interpreteur expected_hash_hex =""# TODO : hash160 du preimagereturn script, result, expected_hash_hex# Testtest_secret =b"mon_secret_pour_atomic_swap"print("Exercice a completer : implementez build_hashlock_script")print(f" Secret : {test_secret.decode()}")print(f" Indice : le script doit verifier hash160(secret) == hash_attendu")
Exercice a completer : implementez build_hashlock_script
Secret : mon_secret_pour_atomic_swap
Indice : le script doit verifier hash160(secret) == hash_attendu
3. Construction de transactions Bitcoin
Une transaction Bitcoin brute est une structure binaire avec un format précis. Cette section montre la construction manuelle d’une transaction, puis l’utilisation optionnelle de python-bitcoinlib pour la version complete avec signature.
import hashlibimport structimport timeclass RawTransaction:"""Construction manuelle d'une transaction Bitcoin brute (simplifiee)."""def__init__(self, version=1):self.version = versionself.inputs = []self.outputs = []self.locktime =0# 0 = pas de timelockdef add_input(self, prev_tx_hash, prev_output_index, script_sig=b""):"""Ajouter un input (reference a un UTXO existant)."""self.inputs.append({"prev_tx": bytes.fromhex(prev_tx_hash),"prev_index": prev_output_index,"script_sig": script_sig,"sequence": 0xFFFFFFFF, # pas de RBF })def add_output(self, amount_satoshis, script_pubkey=b""):"""Ajouter un output (nouveau UTXO)."""self.outputs.append({"amount": amount_satoshis,"script_pubkey": script_pubkey, })def serialize(self):"""Serialiser la transaction au format binaire Bitcoin.""" raw =b""# Version (4 bytes, little-endian) raw += struct.pack("<I", self.version)# Nombre d'inputs (varint) raw +=bytes([len(self.inputs)])for inp inself.inputs: raw += inp["prev_tx"][::-1] # hash en little-endian raw += struct.pack("<I", inp["prev_index"]) raw +=bytes([len(inp["script_sig"])]) + inp["script_sig"] raw += struct.pack("<I", inp["sequence"])# Nombre d'outputs (varint) raw +=bytes([len(self.outputs)])for out inself.outputs: raw += struct.pack("<q", out["amount"]) # montant en satoshis raw +=bytes([len(out["script_pubkey"])]) + out["script_pubkey"]# Locktime (4 bytes) raw += struct.pack("<I", self.locktime)return rawdef txid(self):"""Calculer le txid (double SHA-256 de la transaction serialisee).""" raw =self.serialize()return hashlib.sha256(hashlib.sha256(raw).digest()).hexdigest()# Construire une transaction manuellementtx = RawTransaction(version=2)# Input : reference a un UTXO fictifprev_tx ="a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"tx.add_input(prev_tx, 0)# Output 1 : envoyer 0.5 BTC (50,000,000 satoshis) a Bobbob_script =b"\x76\xa9"+ hash160(b"bob_pubkey") +b"\x88\xac"# P2PKHtx.add_output(50_000_000, bob_script)# Output 2 : monnaie rendue a Alice (0.49 BTC)alice_script =b"\x76\xa9"+ hash160(b"alice_pubkey") +b"\x88\xac"tx.add_output(49_000_000, alice_script)# Frais implicites : 0.01 BTC (1,000,000 satoshis)raw = tx.serialize()print("TRANSACTION BITCOIN BRUTE")print("="*60)print(f"Version : {tx.version}")print(f"Inputs : {len(tx.inputs)}")print(f"Outputs : {len(tx.outputs)}")print(f" Output 0 (Bob) : {tx.outputs[0]['amount']:>12,} satoshis (0.5 BTC)")print(f" Output 1 (Alice) : {tx.outputs[1]['amount']:>12,} satoshis (0.49 BTC)")print(f" Frais implicites : {1_000_000:>12,} satoshis (0.01 BTC)")print(f"Locktime : {tx.locktime}")print(f"Taille brute : {len(raw)} octets")print(f"TxID : {tx.txid()}")print("Structure : [version:4][nb_inputs:1][inputs...][nb_outputs:1][outputs...][locktime:4]")
Interpretation : Construction d’une transaction brute
La serialisation d’une transaction Bitcoin suit un format binaire strict avec un ordre little-endian.
Champ
Taille
Description
version
4 octets
Version du format (1 ou 2)
vin_count
VarInt
Nombre d’inputs
txid + vout
32+4 octets
Reference UTXO source
scriptSig
VarInt + data
Signature de deverrouillage
sequence
4 octets
RBF / timelock relatif
vout_count
VarInt
Nombre d’outputs
value
8 octets
Montant en satoshis
scriptPubKey
VarInt + data
Condition de depense
locktime
4 octets
Timelock absolu
Points cles : - Le txid est le hash SHA-256 double de la transaction serialisee — c’est l’identifiant unique - L’ordre des champs est fixe : pas de format TLV (Type-Length-Value), chaque champ a une position determinee - SegWit (BIP 141) ajoute un champ witness après les inputs, mais le txid classique exclut les données witness - Toute erreur d’un seul octet invalide la transaction entiere — la construction manuelle est donc educative mais risquee en production
Utilisation de la bibliothèque python-bitcoinlib pour la construction et la manipulation de transactions Bitcoin.
# Utilisation de python-bitcoinlib (optionnel)# Cette bibliotheque fournit les types Bitcoin natifstry:import bitcoinfrom bitcoin.core import ( CMutableTransaction, CMutableTxIn, CMutableTxOut, COutPoint, lx )from bitcoin.core.script import ( CScript, OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG )from bitcoin.wallet import CBitcoinSecret, P2PKHBitcoinAddress# Selectionner le reseau testnet bitcoin.SelectParams("testnet")# Generer une cle privee secret_bytes = hashlib.sha256(b"cle_demo_ne_pas_utiliser_en_production").digest() secret = CBitcoinSecret.from_secret_bytes(secret_bytes) address = P2PKHBitcoinAddress.from_pubkey(secret.pub)print("PYTHON-BITCOINLIB")print("="*60)print(f"Cle privee (WIF) : {secret}")print(f"Cle publique : {secret.pub.hex()}")print(f"Adresse (testnet): {address}")print()# Creer une transaction avec la bibliotheque txin = CMutableTxIn( COutPoint(lx("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"), 0) )# Script P2PKH standard script_pubkey = CScript([ OP_DUP, OP_HASH160, bitcoin.core.Hash160(secret.pub), OP_EQUALVERIFY, OP_CHECKSIG ]) txout = CMutableTxOut(50_000_000, script_pubkey) tx = CMutableTransaction([txin], [txout])print(f"Transaction creee avec python-bitcoinlib")print(f" Taille : {len(tx.serialize())} octets")print(f" Script : {script_pubkey.hex()}")exceptImportError:print("python-bitcoinlib non installe.")print("Pour l'installer : pip install python-bitcoinlib")exceptExceptionas e:# python-bitcoinlib necessite libsecp256k1 (natif) qui peut manquer sous Windowsprint(f"python-bitcoinlib installe mais erreur au chargement : {type(e).__name__}")print(f" {e}")print()print("Sur Windows, libsecp256k1 peut ne pas etre disponible.")print("Les sections precedentes (UTXO, Script interpreter, Raw TX)")print("utilisent du Python pur et fonctionnent sans cette dependance.")print("Pour un support complet : utiliser le kernel WSL SmartContracts.")
python-bitcoinlib non installe.
Pour l'installer : pip install python-bitcoinlib
Interpretation : Transactions Bitcoin
La structure d’une transaction Bitcoin est remarquablement simple :
+----------+--------+--------------------+--------+---------------------+----------+
| Version | Nb In | Inputs | Nb Out | Outputs | Locktime |
| 4 bytes | varint | (prev_tx + script) | varint | (amount + script) | 4 bytes |
+----------+--------+--------------------+--------+---------------------+----------+
Points cles : - Les montants sont en satoshis (1 BTC = 100 000 000 satoshis) - Les frais ne sont pas explicites : c’est la différence entre inputs et outputs - Le txid est le double SHA-256 de la transaction serialisee - locktime permet de retarder la validite d’une transaction
4. Scripts avances
Bitcoin Script permet des schemas de transaction plus sophistiques que le simple P2PKH. Ces mécanismes sont la base des “contrats” Bitcoin, bien plus limites que les smart contracts Ethereum mais suffisants pour de nombreux cas d’usage.
# === MULTISIG (2-of-3) ===# Necessite 2 signatures parmi 3 cles autorisees# Utilise pour : coffres d'entreprise, escrow, wallets partagesprint("SCRIPTS AVANCES")print("="*60)print()print("--- 4.1 Multisig (2-of-3) ---")print()# Cles publiques des 3 signataireskeys = {"Alice": hashlib.sha256(b"alice_key").hexdigest()[:40],"Bob": hashlib.sha256(b"bob_key").hexdigest()[:40],"Carol": hashlib.sha256(b"carol_key").hexdigest()[:40],}# Script de verrouillage multisigmultisig_script = ["OP_2", # M = 2 signatures requises keys["Alice"], # cle publique 1 keys["Bob"], # cle publique 2 keys["Carol"], # cle publique 3"OP_3", # N = 3 cles au total"OP_CHECKMULTISIG", # verifier M-of-N]print("scriptPubKey (verrouillage) :")for item in multisig_script:if item.startswith("OP_"):print(f" {item}")else:# Trouver le nom de la cle name = [k for k, v in keys.items() if v == item][0]print(f" <{name}_pubkey> ({item[:16]}...)")print()print("scriptSig (deverrouillage, signe par Alice et Carol) :")print(" OP_0 (bug historique de Bitcoin)")print(" <sig_Alice>")print(" <sig_Carol>")print()print("-> 2 signatures sur 3 suffisent pour depenser l'UTXO")print("-> Cas d'usage : Alice + Bob = depense normale")print(" Alice + Carol = si Bob est indisponible")print(" Bob + Carol = arbitrage sans Alice")
SCRIPTS AVANCES
============================================================
--- 4.1 Multisig (2-of-3) ---
scriptPubKey (verrouillage) :
OP_2
<Alice_pubkey> (d129fa7166981535...)
<Bob_pubkey> (8ef731e6c291f4d8...)
<Carol_pubkey> (53209930882aa035...)
OP_3
OP_CHECKMULTISIG
scriptSig (deverrouillage, signe par Alice et Carol) :
OP_0 (bug historique de Bitcoin)
<sig_Alice>
<sig_Carol>
-> 2 signatures sur 3 suffisent pour depenser l'UTXO
-> Cas d'usage : Alice + Bob = depense normale
Alice + Carol = si Bob est indisponible
Bob + Carol = arbitrage sans Alice
Interpretation : Multi-signatures (Multisig)
Le multisig repartit le contrôle des fonds entre plusieurs parties, eliminant le point unique de defaillance.
Paramètre
Signification
Securite
OP_2
Seuil requis
2 signatures sur 3 cles
<pubkey1>...<pubkey3>
Cles autorisees
Chaque partie contrôle sa cle
OP_3
Nombre total de cles
Redondance 1/n
Points cles : - Le script natif OP_2 <pk1> <pk2> <pk3> OP_3 OP_CHECKMULTISIG exige exactement 2 signatures parmi 3 cles - Bare multisig (P2MS) : le script complet apparait dans le scriptPubKey — lisibilite totale mais cout eleve - Le bug historique OP_CHECKMULTISIG consomme un élément de pile supplementaire (off-by-one), d’ou le OP_0 initial - En pratique moderne, le multisig est encapsule dans P2SH ou P2WSH pour reduire les frais et ameliorer la confidentialite
Présentation des scripts TIMELOCK (CLTV et CSV) pour verrouiller des fonds jusqu’à une date ou un nombre de blocs défini.
# === TIMELOCK (CLTV et CSV) ===# CLTV = CheckLockTimeVerify : verrouillage jusqu'a un bloc ou une date# CSV = CheckSequenceVerify : verrouillage relatif (N blocs apres confirmation)import timeprint("--- 4.2 Timelock (CLTV / CSV) ---")print()# Hauteur de bloc actuelle simuleecurrent_block =840_000# Approximation avril 2024current_time =int(time.time())# CLTV : ne peut pas etre depense avant le bloc 850,000cltv_script = [850_000, # hauteur de bloc cible"OP_CHECKLOCKTIMEVERIFY", # echoue si bloc < 850,000"OP_DROP", # retirer la valeur de la pile# ... suivi du script P2PKH normal"OP_DUP", "OP_HASH160", "<pubkey_hash>", "OP_EQUALVERIFY", "OP_CHECKSIG"]# CSV : ne peut pas etre depense avant 144 blocs (~24h) apres confirmationcsv_blocks =144# ~24 heurescsv_script = [ csv_blocks, # nombre de blocs relatif"OP_CHECKSEQUENCEVERIFY", # echoue si sequence < 144"OP_DROP","OP_DUP", "OP_HASH160", "<pubkey_hash>", "OP_EQUALVERIFY", "OP_CHECKSIG"]print("CLTV (CheckLockTimeVerify) - Timelock absolu :")print(f" Bloc actuel : {current_block:,}")print(f" Bloc cible : 850,000")remaining =850_000- current_blockprint(f" Blocs restants : {remaining:,} (~{remaining *10//60} heures)")if current_block <850_000:print(" Status : VERROUILLE")else:print(" Status : DEVERROUILLE")print()print("CSV (CheckSequenceVerify) - Timelock relatif :")print(f" Delai requis : {csv_blocks} blocs (~24 heures)")print(f" Usage typique : delai de securite apres depot")print()print("Comparaison des timelocks :")fmt =" {:<10}{:<20}{}"print(fmt.format("Type", "Reference", "Usage courant"))print(fmt.format("CLTV", "Bloc ou timestamp", "Heritage, paris, echeances"))print(fmt.format("CSV", "Blocs relatifs", "Lightning Network, securite"))print(fmt.format("nLocktime", "Champ de la tx", "Transactions differees"))
Les timelocks ajoutent une dimension temporelle aux transactions Bitcoin, essentielle pour les canaux de paiement et les contrats conditionnels.
Type
Opcode
Granularite
Cas d’usage
CLTV
OP_CHECKLOCKTIMEVERIFY
Date absolue (timestamp/bloc)
Heritage, vesting, escrow
CSV
OP_CHECKSEQUENCEVERIFY
Relative (depuis confirmation)
Lightning Network, RBF
Points cles : - CLTV bloque les fonds jusqu’a un bloc ou timestamp spécifique — equivalent d’un “compte a terme” - CSV impose un delai relatif après confirmation — ideal pour les canaux de paiement bidirectionnels - Ces opcodes ne font que verifier la condition ; le consensus Bitcoin enforcing le verrou via nLockTime (CLTV) et nSequence (CSV) - Lightning Network utilise CSV pour garantir que le canal peut etre ferme unilateralement, mais avec un delai de contestation pour l’autre partie
Illustration du format P2SH (Pay-to-Script-Hash) permettant d’encapsuler des scripts complexes dans une adresse standard.
# === P2SH (Pay-to-Script-Hash) ===# BIP 16 : au lieu de mettre le script complet dans l'output,# on met seulement son hash. Le script complet est revele au moment# de depenser.print("--- 4.3 P2SH (Pay-to-Script-Hash) ---")print()# Le script multisig 2-of-3 est long et couteux en espace# Avec P2SH, on le remplace par son hashredeem_script ="OP_2 <Alice> <Bob> <Carol> OP_3 OP_CHECKMULTISIG"redeem_hash = hash160(redeem_script.encode())print("Sans P2SH (multisig brut dans scriptPubKey) :")print(f" scriptPubKey : OP_2 <pk1> <pk2> <pk3> OP_3 OP_CHECKMULTISIG")print(f" Taille script : ~{len(redeem_script)} octets")print(f" -> L'expediteur paie pour la complexite du script")print()print("Avec P2SH (hash du script dans scriptPubKey) :")print(f" scriptPubKey : OP_HASH160 {redeem_hash.hex()[:20]}... OP_EQUAL")print(f" Taille script : ~23 octets (toujours la meme)")print(f" -> L'expediteur paie un cout fixe, peu importe la complexite")print()print("Pour depenser (scriptSig) :")print(f" OP_0 <sig_Alice> <sig_Bob> <redeem_script_complet>")print(f" -> Le script complet est revele seulement au moment de depenser")print(f" -> Le destinataire paie la complexite")print()print("Avantages de P2SH :")print(" 1. Adresses P2SH commencent par '3' (vs '1' pour P2PKH)")print(" 2. Taille fixe de l'output, quelle que soit la complexite du script")print(" 3. La complexite est cachee jusqu'a la depense")print(" 4. Le destinataire choisit les conditions de depense")
--- 4.3 P2SH (Pay-to-Script-Hash) ---
Sans P2SH (multisig brut dans scriptPubKey) :
scriptPubKey : OP_2 <pk1> <pk2> <pk3> OP_3 OP_CHECKMULTISIG
Taille script : ~48 octets
-> L'expediteur paie pour la complexite du script
Avec P2SH (hash du script dans scriptPubKey) :
scriptPubKey : OP_HASH160 a7419735eccca221c2dc... OP_EQUAL
Taille script : ~23 octets (toujours la meme)
-> L'expediteur paie un cout fixe, peu importe la complexite
Pour depenser (scriptSig) :
OP_0 <sig_Alice> <sig_Bob> <redeem_script_complet>
-> Le script complet est revele seulement au moment de depenser
-> Le destinataire paie la complexite
Avantages de P2SH :
1. Adresses P2SH commencent par '3' (vs '1' pour P2PKH)
2. Taille fixe de l'output, quelle que soit la complexite du script
3. La complexite est cachee jusqu'a la depense
4. Le destinataire choisit les conditions de depense
Interpretation : Scripts avances
Type
Complexite
Cas d’usage
Adresse
P2PKH
Simple
Paiement standard
Commence par 1
Multisig
M-of-N
Coffres partages, escrow
Integre dans P2SH
CLTV
Timelock absolu
Heritage, paris, echeances
Integre dans P2SH
CSV
Timelock relatif
Lightning Network, securite
Integre dans P2SH
P2SH
Hash de script
Encapsule tout script complexe
Commence par 3
P2WSH
SegWit
Version moderne de P2SH
Commence par bc1
Note : Depuis 2021, Taproot (BIP 340-342) ajoute les signatures Schnorr et les arbres MAST, permettant des scripts encore plus flexibles et confidentiels. Les adresses Taproot commencent par bc1p.
4b. Exercice : Construction d’un script P2SH pour heritage
Objectif
Construisez un script P2SH (Pay-to-Script-Hash) pour un système d’heritage Bitcoin. Les fonds sont verrouilles selon les règles suivantes :
Branche 1 : Alice peut depenser seule après un timelock de 1000 blocs (CLTV)
Branche 2 : Bob et Carol (2-of-2 multisig) peuvent depenser a tout moment (en cas de deces d’Alice)
Contexte
Ce type de script est utilise pour planifier la transmission d’heritage : le proprietaire (Alice) accede a ses fonds après une certaine date, mais ses heritiers (Bob + Carol ensemble) peuvent y acceder immediatement si Alice n’est plus en mesure de gerer ses fonds.
Indice :
Utilisez OP_IF / OP_ELSE / OP_ENDIF pour créer les deux branches : - Branche OP_IF : CLTV + signature d’Alice - Branche OP_ELSE : multisig 2-of-2 (Bob + Carol)
La fonction hash160 est disponible pour calculer le hash du redeem script.
def build_inheritance_script(alice_pubkey, bob_pubkey, carol_pubkey, timeout_blocks):"""Construire le redeem script d'un heritage Bitcoin.TODO etudiant : construisez une liste representant le script Bitcoin : - OP_IF : branche heritage (Alice seule apres timeout) - <timeout_blocks> OP_CHECKLOCKTIMEVERIFY OP_DROP - <alice_pubkey> OP_CHECKSIG - OP_ELSE : branche multisig heritiers (Bob + Carol, 2-of-2) - OP_2 <bob_pubkey> <carol_pubkey> OP_2 OP_CHECKMULTISIG - OP_ENDIF Args: alice_pubkey: cle publique d'Alice (str) bob_pubkey: cle publique de Bob (str) carol_pubkey: cle publique de Carol (str) timeout_blocks: nombre de blocs pour le timelock (int) Returns: tuple: (redeem_script, script_hash, p2sh_address_prefix) """# TODO etudiant : construisez le redeem script redeem_script = [] # TODO : remplacez par votre script# Calculer le hash du script (fourni si redeem_script est correct)if redeem_script: script_str ="|".join(str(item) for item in redeem_script) script_hash = hash160(script_str.encode())return redeem_script, script_hash.hex(), "3..."return [], "", ""# Cles simuleesalice_pk = hashlib.sha256(b"alice_inheritance").hexdigest()[:40]bob_pk = hashlib.sha256(b"bob_heir").hexdigest()[:40]carol_pk = hashlib.sha256(b"carol_heir").hexdigest()[:40]print("Exercice a completer : implementez build_inheritance_script")print(f" Alice : {alice_pk[:16]}...")print(f" Bob : {bob_pk[:16]}...")print(f" Carol : {carol_pk[:16]}...")print(f" Timeout : 1000 blocs")
Exercice a completer : implementez build_inheritance_script
Alice : f537170981c4af53...
Bob : 573e20a198d6f79e...
Carol : 75265dedfedb125d...
Timeout : 1000 blocs
5. Bitcoin vs Ethereum : deux philosophies
Bitcoin et Ethereum representent deux approches fondamentalement différentes de la programmabilite blockchain.
Comparaison architecturale
Aspect
Bitcoin
Ethereum
Modèle d’etat
UTXO
Account
Langage
Bitcoin Script (pile)
Solidity/Vyper (Turing-complet)
Turing-completude
Non (pas de boucles)
Oui (avec gas limit)
Etat persistent
Non (stateless)
Oui (storage)
Exécution
Verification (valide/invalide)
Computation (exécution complete)
Gas/Frais
Par octet de transaction
Par instruction executee
Expressivite
Limitee (conditions de depense)
Maximale (programmes arbitraires)
Securite
Minimale surface d’attaque
Surface d’attaque large (reentrancy, etc.)
Avantages de Bitcoin Script
Securite par restriction : impossible de créer de bugs complexes
Verification rapide : l’exécution se termine toujours en temps borne
Confidentialite : le script n’est revele qu’a la depense (P2SH)
Previsibilite des frais : proportionnels a la taille, pas a la complexite
Avantages d’Ethereum/Solidity
Expressivite : tout algorithme peut etre encode
Etat persistent : le contrat “se souvient” entre les appels
Composabilite : les contrats peuvent s’appeler entre eux (DeFi)
Bitcoin : Taproot (2021) ajoute plus de flexibilite, Ordinals/Inscriptions poussent les limites du script
Ethereum : les L2 (rollups) utilisent des preuves de validite similaires a l’approche Bitcoin (verification > computation)
Les deux : convergent vers un modèle ou la L1 verifie et les L2 executent
6. Exemple guide : Escrow avec timelock — Solution etudiante (Jules Lange, TP 2026)
Un système d’escrow (tiers de confiance) combinant multisig et timelock en utilisant les concepts de Bitcoin Script.
Scénario
Alice achete un produit a Bob. L’argent est verrouille dans un escrow : - Cas normal : Alice ET Bob signent pour liberer les fonds vers Bob - Litige : Alice ET Carol (arbitre) signent pour rembourser Alice - Timeout : après 1000 blocs sans action, Bob peut recuperer les fonds seul
Implementation
La classe TimelockEscrow implemente les trois scénarios via un script de verrouillage conditionnel (OP_IF/OP_ELSE/OP_ENDIF) combinant multisig 2-of-3 et timelock CLTV.
import hashlibimport timedef sign(pubkey):"""Generer une signature simulee a partir d'une cle publique."""ifisinstance(pubkey, str): pubkey = pubkey.encode()return hashlib.sha256(pubkey).hexdigest()[:16]def valid_signature(signature, pubkey):"""Verifier qu'une signature correspond a une cle publique."""return signature == sign(pubkey)class TimelockEscrow:"""Escrow avec timelock combinant multisig et CLTV. Regles : - Alice + Bob : liberation immediate vers Bob - Alice + Carol (arbitre) : remboursement vers Alice - Bob seul apres timeout : recuperation par Bob Source : solution etudiante (Jules Lange, TP 2026). """def__init__(self, alice_pubkey, bob_pubkey, carol_pubkey, amount, timeout_blocks=1000):self.alice = alice_pubkeyself.bob = bob_pubkeyself.carol = carol_pubkeyself.amount = amountself.timeout_blocks = timeout_blocksself.creation_block =0self.is_spent =Falsedef get_redeem_script(self):"""Construire le script de verrouillage Bitcoin Script. Structure : - OP_IF : branche multisig 2-of-3 (Alice, Bob, Carol) - OP_ELSE : branche timeout (Bob seul apres N blocs) - OP_ENDIF """return ["OP_IF","OP_2", self.alice, self.bob, self.carol, "OP_3", "OP_CHECKMULTISIG","OP_ELSE",self.timeout_blocks, "OP_CHECKLOCKTIMEVERIFY", "OP_DROP",self.bob, "OP_CHECKSIG","OP_ENDIF", ]def _check_multisig(self, signatures, signers):"""Verifier une combinaison de signatures 2-of-3.""" keys = [self.alice, self.bob, self.carol] valid =0 seen =set()for sig, signer inzip(signatures, signers):if signer in keys and signer notin seen and valid_signature(sig, signer): valid +=1 seen.add(signer)return valid >=2def spend_multisig(self, sig_a, signer_a, sig_b, signer_b):"""Depenser via la branche multisig (2-of-3)."""ifself.is_spent:raiseRuntimeError("Escrow deja depense")ifnotself._check_multisig([sig_a, sig_b], [signer_a, signer_b]):raiseRuntimeError("Multisig 2-of-3 echoue") signers = {signer_a, signer_b}if signers == {self.alice, self.bob}: recipient ="Bob"elif signers == {self.alice, self.carol}: recipient ="Alice"else: recipient ="Bob"self.is_spent =Truereturn recipientdef spend_timeout(self, sig_bob, current_block):"""Depenser via la branche timeout (Bob seul apres N blocs)."""ifself.is_spent:raiseRuntimeError("Escrow deja depense")if current_block <self.creation_block +self.timeout_blocks:raiseRuntimeError("Timeout non atteint")ifnot valid_signature(sig_bob, self.bob):raiseRuntimeError("Signature de Bob invalide")self.is_spent =Truereturn"Bob"# --- Tests des 3 scenarios ---print("TIMELOCK ESCROW - Test des 3 scenarios")print("="*60)# Generation des clesalice_key = hashlib.sha256(b"alice_escrow_key").hexdigest()[:40]bob_key = hashlib.sha256(b"bob_escrow_key").hexdigest()[:40]carol_key = hashlib.sha256(b"carol_escrow_key").hexdigest()[:40]escrow = TimelockEscrow(alice_key, bob_key, carol_key, amount=10, timeout_blocks=1000)print(f"Escrow cree : {escrow.amount} BTC, timeout = {escrow.timeout_blocks} blocs")print(f"Redeem script : {len(escrow.get_redeem_script())} elements")print()# Scenario 1 : cas normal (Alice + Bob -> Bob)print("--- Scenario 1 : Cas normal (Alice + Bob) ---")sig_alice = sign(alice_key)sig_bob = sign(bob_key)recipient = escrow.spend_multisig(sig_alice, alice_key, sig_bob, bob_key)print(f"Multisig valide, fonds envoyes a : {recipient}")print()# Scenario 2 : litige (Alice + Carol -> Alice)print("--- Scenario 2 : Litige (Alice + Carol -> Alice) ---")escrow2 = TimelockEscrow(alice_key, bob_key, carol_key, amount=10, timeout_blocks=1000)sig_alice2 = sign(alice_key)sig_carol = sign(carol_key)recipient2 = escrow2.spend_multisig(sig_alice2, alice_key, sig_carol, carol_key)print(f"Arbitrage, fonds rembourses a : {recipient2}")print()# Scenario 3 : timeout (Bob seul apres 1000 blocs)print("--- Scenario 3 : Timeout (Bob seul apres 1000 blocs) ---")escrow3 = TimelockEscrow(alice_key, bob_key, carol_key, amount=10, timeout_blocks=1000)escrow3.creation_block =100sig_bob2 = sign(bob_key)recipient3 = escrow3.spend_timeout(sig_bob2, current_block=1200)print(f"Timeout atteint, fonds recuperes par : {recipient3}")print()print("=> Les 3 scenarios fonctionnent correctement")
TIMELOCK ESCROW - Test des 3 scenarios
============================================================
Escrow cree : 10 BTC, timeout = 1000 blocs
Redeem script : 14 elements
--- Scenario 1 : Cas normal (Alice + Bob) ---
Multisig valide, fonds envoyes a : Bob
--- Scenario 2 : Litige (Alice + Carol -> Alice) ---
Arbitrage, fonds rembourses a : Alice
--- Scenario 3 : Timeout (Bob seul apres 1000 blocs) ---
Timeout atteint, fonds recuperes par : Bob
=> Les 3 scenarios fonctionnent correctement
6b. Exemple guide (avance) : Escrow avec penalite et delai de grace
Cet exemple pousse plus loin le système d’escrow de la section 6 en lui ajoutant des mécanismes de protection avances : un delai de grace qui encadre la recuperation des fonds après le timeout.
Scénario
Dans un escrow de commerce electronique, on souhaite encadrer la recuperation après timeout : - Delai de grace : après le timeout_blocks, Bob dispose d’une fenêtre de grace_period blocs (200 par defaut) pour recuperer les fonds via sa signature - Expiration : une fois cette fenêtre ecoulee, Alice peut recuperer ses fonds, Bob n’a plus acces
Implementation
La classe GracePeriodEscrow (cellule suivante) etend TimelockEscrow avec deux méthodes de depense distinctes : - spend_grace_timeout(sig_bob, current_block) : Bob recupere les fonds dans la fenêtre [timeout_blocks, timeout_blocks + grace_period[ - spend_expired(sig_alice, current_block) : Alice recupere les fonds après expiration du delai de grace
Chaque méthode verifie l’etat de l’escrow, la position de current_block par rapport aux seuils, puis la signature du reclamant. Les quatre scénarios de test (recuperation dans le delai, recuperation après expiration, rejet hors fenêtre, rejet avant timeout) valident le comportement attendu.
Cet exemple guide est la suite naturelle du worked-example TimelockEscrow de la section 6 : il montre comment composer timelock et fenêtre de recuperation pour realiser un escrow de commerce electronique realiste. Les trois exercices a completer (sections 1b, 2b, 4b) restent independants.
class GracePeriodEscrow:"""Escrow avec delai de grace apres timeout. Regles : - Alice + Bob : liberation immediate vers Bob (multisig 2-of-3) - Alice + Carol : arbitrage, remboursement vers Alice - Bob seul apres timeout_blocks : recuperation (delai de grace = 200 blocs) - Alice seule apres timeout_blocks + grace_period : recuperation si Bob n'a pas agi """def__init__(self, alice_pubkey, bob_pubkey, carol_pubkey, amount, timeout_blocks=1000, grace_period=200):self.alice = alice_pubkeyself.bob = bob_pubkeyself.carol = carol_pubkeyself.amount = amountself.timeout_blocks = timeout_blocksself.grace_period = grace_periodself.creation_block =0self.is_spent =Falsedef spend_grace_timeout(self, sig_bob, current_block):"""Bob recupere les fonds apres timeout (dans le delai de grace). Conditions : 1. L'escrow n'est pas deja depense 2. Le timeout est atteint (current_block >= creation_block + timeout_blocks) 3. Le delai de grace n'est pas expire (current_block < creation_block + timeout_blocks + grace_period) 4. La signature de Bob est valide """ifself.is_spent:raiseRuntimeError("Escrow deja depense")if current_block <self.creation_block +self.timeout_blocks:raiseRuntimeError(f"Timeout non atteint : bloc {current_block} < {self.creation_block +self.timeout_blocks}" )if current_block >=self.creation_block +self.timeout_blocks +self.grace_period:raiseRuntimeError(f"Delai de grace expire au bloc {self.creation_block +self.timeout_blocks +self.grace_period}" )ifnot valid_signature(sig_bob, self.bob):raiseRuntimeError("Signature de Bob invalide")self.is_spent =Truereturn"Bob"def spend_expired(self, sig_alice, current_block):"""Alice recupere les fonds apres expiration du delai de grace. Conditions : 1. L'escrow n'est pas deja depense 2. Le delai de grace est expire (current_block >= creation_block + timeout_blocks + grace_period) 3. La signature d'Alice est valide """ifself.is_spent:raiseRuntimeError("Escrow deja depense")if current_block <self.creation_block +self.timeout_blocks +self.grace_period:raiseRuntimeError(f"Delai de grace pas encore expire : bloc {current_block} < {self.creation_block +self.timeout_blocks +self.grace_period}" )ifnot valid_signature(sig_alice, self.alice):raiseRuntimeError("Signature d'Alice invalide")self.is_spent =Truereturn"Alice"# --- Tests ---print("GracePeriodEscrow - Test des scenarios")print("="*60)alice_key = hashlib.sha256(b"alice_escrow_key").hexdigest()[:40]bob_key = hashlib.sha256(b"bob_escrow_key").hexdigest()[:40]carol_key = hashlib.sha256(b"carol_escrow_key").hexdigest()[:40]# Scenario 1 : Bob recupere dans le delai de grace (bloc 1050, grace [1000, 1200[)print("--- Scenario 1 : Bob recupere dans le delai de grace ---")e1 = GracePeriodEscrow(alice_key, bob_key, carol_key, amount=5)e1.creation_block =0recipient1 = e1.spend_grace_timeout(sign(bob_key), current_block=1050)print(f"Fonds recuperes par : {recipient1} (bloc 1050, dans [1000, 1200[)")print()# Scenario 2 : Alice recupere apres expiration du delai de grace (bloc 1250)print("--- Scenario 2 : Alice recupere apres expiration du delai de grace ---")e2 = GracePeriodEscrow(alice_key, bob_key, carol_key, amount=5)e2.creation_block =0recipient2 = e2.spend_expired(sign(alice_key), current_block=1250)print(f"Fonds recuperes par : {recipient2} (bloc 1250 >= 1000 + 200)")print()# Scenario 3 : Bob arrive trop tard (apres le delai de grace) -> rejetprint("--- Scenario 3 : Bob arrive apres le delai de grace (rejet attendu) ---")e3 = GracePeriodEscrow(alice_key, bob_key, carol_key, amount=5)e3.creation_block =0try: e3.spend_grace_timeout(sign(bob_key), current_block=1300)print("ERREUR : aurait du etre rejete")exceptRuntimeErroras err:print(f"Rejet correct : {err}")print()# Scenario 4 : Bob essaie avant le timeout -> rejetprint("--- Scenario 4 : Bob essaie avant le timeout (rejet attendu) ---")e4 = GracePeriodEscrow(alice_key, bob_key, carol_key, amount=5)e4.creation_block =0try: e4.spend_grace_timeout(sign(bob_key), current_block=500)print("ERREUR : aurait du etre rejete")exceptRuntimeErroras err:print(f"Rejet correct : {err}")print()print("=> Les 4 scenarios fonctionnent correctement")
GracePeriodEscrow - Test des scenarios
============================================================
--- Scenario 1 : Bob recupere dans le delai de grace ---
Fonds recuperes par : Bob (bloc 1050, dans [1000, 1200[)
--- Scenario 2 : Alice recupere apres expiration du delai de grace ---
Fonds recuperes par : Alice (bloc 1250 >= 1000 + 200)
--- Scenario 3 : Bob arrive apres le delai de grace (rejet attendu) ---
Rejet correct : Delai de grace expire au bloc 1200
--- Scenario 4 : Bob essaie avant le timeout (rejet attendu) ---
Rejet correct : Timeout non atteint : bloc 500 < 1000
=> Les 4 scenarios fonctionnent correctement
7. Resume
Concept
Description
Implementation
UTXO
Modèle de pieces non depensees
Classe UTXOSet avec coin sélection
Bitcoin Script
Langage a pile, Turing-incomplet
Interpreteur avec 12 opcodes
P2PKH
Paiement standard a un hash de cle publique
DUP + HASH160 + EQUALVERIFY + CHECKSIG
Transaction brute
Structure binaire serialisee
Version + inputs + outputs + locktime
Multisig
M-of-N signatures requises
2-of-3 pour coffres partages
Timelock
Verrouillage temporel (CLTV/CSV)
Bloc absolu ou relatif
P2SH
Hash du script dans l’output
Taille fixe, complexite cachee
Points cles
Le modèle UTXO offre un parallelisme naturel et une meilleure confidentialite que le modèle Account
Bitcoin Script est volontairement limite pour maximiser la securite
Les scripts avances (multisig, timelock, P2SH) couvrent la majorite des besoins en conditions de depense
La philosophie de Bitcoin est verification, celle d’Ethereum est computation
Ce notebook a explore le modèle UTXO de Bitcoin et son langage Script a pile, volontairement Turing-incomplet. Nous avons implemente un mini-interpreteur capable d’executer des scripts P2PKH, construit des transactions brutes en serialisant leurs champs binaires, et decouvert les mécanismes avances que sont le multisig (M-of-N), les timelocks (CLTV/CSV) et l’encapsulation P2SH. La comparaison avec Ethereum met en lumiere deux philosophies opposees : la verification déterministe et bornee de Bitcoin contre la computation expressive mais risque d’Ethereum.
Les scripts Bitcoin, malgre leur simplicite apparente, couvrent la majorite des besoins en conditions de depense : signatures multiples, verrous temporels, et logique conditionnelle. L’arrivee de Taproot (BIP 340-342) en 2021 enrichit ce repertoire avec les signatures Schnorr et les arbres MAST, ouvrant la voie a des scripts plus flexibles tout en preservant la confidentialite. Les adaptations modernes comme les ordinals et les inscriptions montrent que la frontiere entre verification et computation continue d’evolver.
Le parcours SmartContracts s’acheve ici. Depuis les fondements cypherpunk (SC-0) jusqu’au scripting Bitcoin, en passant par Solidity, le test avec Foundry, la verification formelle, la cryptographie avancee (ZKP, chiffrement homomorphique, vote verifiable), les chaînes alternatives (Vyper, Ripple/XRP), vous disposez maintenant d’une vision panoramique des technologies de contrats intelligents et de leur ecosysteme. Le notebook final SC-26-Final-Project-Python vous invite a consolider ces competences dans un projet integrateur.