SC-20-Bitcoin-Scripting-Python - Bitcoin, UTXO et Scripts

<< Ripple XRP | Move & Sui >>


Objectifs d’apprentissage

  1. Comprendre le modèle UTXO de Bitcoin et ses différences avec le modèle Account d’Ethereum
  2. Decouvrir Bitcoin Script, le langage a pile de Bitcoin
  3. Implementer un mini-interpreteur Bitcoin Script en Python
  4. Construire et signer des transactions Bitcoin manuellement
  5. 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 hashlib
import json
from collections import defaultdict


class 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_hash

    def get_balance(self, address):
        """Calculer le solde d'une adresse en sommant ses UTXO."""
        return sum(
            utxo["amount"] for utxo in self.utxos.values()
            if utxo["address"] == address
        )

    def get_utxos_for(self, address):
        """Lister les UTXO d'une adresse."""
        return {
            key: utxo for key, utxo in self.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:
            raise ValueError(
                f"Solde insuffisant: {total_available} < {amount + fee}"
            )

        # Coin selection (simple: on prend les UTXO un par un)
        selected = {}
        selected_total = 0
        for key, utxo in sender_utxos.items():
            selected[key] = utxo
            selected_total += utxo["amount"]
            if selected_total >= amount + fee:
                break

        # Consommer les UTXO selectionnes
        for key in selected:
            del self.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 - fee
        if change > 0:
            tx_data["outputs"].append({"address": sender, "amount": change})

        tx_hash = self._tx_hash(tx_data)

        # Ajouter les nouveaux UTXO
        for i, output in enumerate(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


# Demonstration
utxo_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")
MODELE UTXO - Demonstration
============================================================
Coinbase -> Alice : 50 BTC (tx: 2a07f237163da93c...)
Coinbase -> Alice : 25 BTC (tx: b313f734cdba87c9...)

Solde Alice : 75 BTC
UTXO d'Alice : 2 pieces

Interpretation : Modèle UTXO vs comptes

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 Bob
print("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 Charlie
print(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 :

  1. 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.
  2. Les frais (1 BTC) sont la différence entre inputs et outputs : ils vont implicitement au mineur.
  3. 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 traceur
    return []


# Creation d'un scenario de test
trace_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 hashlib


def hash160(data):
    """HASH160 = RIPEMD160(SHA256(data)), utilise par Bitcoin pour les adresses."""
    sha = hashlib.sha256(data).digest()
    ripemd = hashlib.new("ripemd160", sha).digest()
    return ripemd


class 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."""
        if len(self.stack) < 1:
            raise RuntimeError("Pile vide pour OP_DUP")
        self.stack.append(self.stack[-1])

    def _op_hash160(self):
        """RIPEMD160(SHA256(top))."""
        val = self.stack.pop()
        if isinstance(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()
        if not self.stack.pop():
            raise RuntimeError("OP_EQUALVERIFY echoue")

    def _op_verify(self):
        """Echoue si le sommet est FALSE."""
        if not self.stack.pop():
            raise RuntimeError("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)
        if isinstance(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):
        if len(self.stack) < 2:
            raise RuntimeError("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 in self.opcodes:
                if verbose:
                    print(f"  {item:20s}", end="")
                self.opcodes[item]()
            else:
                if verbose:
                    display = item if not isinstance(item, bytes) else item.hex()[:16] + "..."
                    print(f"  PUSH {str(display):14s}", end="")
                self.stack.append(item)

            if verbose:
                stack_display = []
                for s in self.stack:
                    if isinstance(s, bytes):
                        stack_display.append(s.hex()[:12] + "...")
                    else:
                        stack_display.append(str(s))
                print(f"  pile: {stack_display}")

        return len(self.stack) > 0 and self.stack[-1]


# Demonstration : script arithmetique simple
interp = 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()
MINI-INTERPRETEUR BITCOIN SCRIPT
============================================================

--- Script 1 : Verification arithmetique (2 + 3 == 5) ---
Script : [2, 3, 'OP_ADD', 5, 'OP_EQUAL']
--------------------------------------------------
  PUSH 2               pile: ['2']
  PUSH 3               pile: ['2', '3']
  OP_ADD                pile: ['5']
  PUSH 5               pile: ['5', '5']
  OP_EQUAL              pile: ['True']
Resultat : True

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 format

print("--- Script P2PKH (Pay-to-Public-Key-Hash) ---")
print()

# Simuler une cle publique et son hash
pubkey = "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 + scriptPubKey
full_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")
--- Script P2PKH (Pay-to-Public-Key-Hash) ---

Cle publique  : 02abc123def456...alice_public_...
HASH160(pubkey): 7ecc95de06fe9480cf323db5f11cb06c7511787b
Signature     : aca1fa028cbcf006

Execution du script P2PKH :
(scriptSig + scriptPubKey concatenes)

Script : ['aca1fa028cbcf006', '02abc123def456...alice_public_key', 'OP_DUP', 'OP_HASH160', b'~\xcc\x95\xde\x06\xfe\x94\x80\xcf2=\xb5\xf1\x1c\xb0lu\x11x{', 'OP_EQUALVERIFY', 'OP_CHECKSIG']
--------------------------------------------------
  PUSH aca1fa028cbcf006  pile: ['aca1fa028cbcf006']
  PUSH 02abc123def456...alice_public_key  pile: ['aca1fa028cbcf006', '02abc123def456...alice_public_key']
  OP_DUP                pile: ['aca1fa028cbcf006', '02abc123def456...alice_public_key', '02abc123def456...alice_public_key']
  OP_HASH160            pile: ['aca1fa028cbcf006', '02abc123def456...alice_public_key', '7ecc95de06fe...']
  PUSH 7ecc95de06fe9480...  pile: ['aca1fa028cbcf006', '02abc123def456...alice_public_key', '7ecc95de06fe...', '7ecc95de06fe...']
  OP_EQUALVERIFY        pile: ['aca1fa028cbcf006', '02abc123def456...alice_public_key']
  OP_CHECKSIG           pile: ['True']

Resultat : True
-> La transaction est autorisee : le detenteur de la cle privee peut depenser l'UTXO

Interpretation : Bitcoin Script

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” :

scriptPubKey : OP_HASH160 <expected_hash> OP_EQUALVERIFY
scriptSig    : <preimage>

Indice :

  • 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 preimage

    return script, result, expected_hash_hex


# Test
test_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 hashlib
import struct
import time


class RawTransaction:
    """Construction manuelle d'une transaction Bitcoin brute (simplifiee)."""

    def __init__(self, version=1):
        self.version = version
        self.inputs = []
        self.outputs = []
        self.locktime = 0  # 0 = pas de timelock

    def 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 in self.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 in self.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 raw

    def 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 manuellement
tx = RawTransaction(version=2)

# Input : reference a un UTXO fictif
prev_tx = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
tx.add_input(prev_tx, 0)

# Output 1 : envoyer 0.5 BTC (50,000,000 satoshis) a Bob
bob_script = b"\x76\xa9" + hash160(b"bob_pubkey") + b"\x88\xac"  # P2PKH
tx.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]")
TRANSACTION BITCOIN BRUTE
============================================================
Version  : 2
Inputs   : 1
Outputs  : 2
  Output 0 (Bob)   :   50,000,000 satoshis (0.5 BTC)
  Output 1 (Alice) :   49,000,000 satoshis (0.49 BTC)
  Frais implicites :    1,000,000 satoshis (0.01 BTC)
Locktime : 0
Taille brute  : 117 octets
TxID          : 58476c5ab2fdffe592f7b80c321c59a3345b8fa4f9bae5a5decb448a2a65d52c
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 natifs

try:
    import bitcoin
    from 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()}")

except ImportError:
    print("python-bitcoinlib non installe.")
    print("Pour l'installer : pip install python-bitcoinlib")
except Exception as e:
    # python-bitcoinlib necessite libsecp256k1 (natif) qui peut manquer sous Windows
    print(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 partages

print("SCRIPTS AVANCES")
print("=" * 60)
print()
print("--- 4.1 Multisig (2-of-3) ---")
print()

# Cles publiques des 3 signataires
keys = {
    "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 multisig
multisig_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 time

print("--- 4.2 Timelock (CLTV / CSV) ---")
print()

# Hauteur de bloc actuelle simulee
current_block = 840_000  # Approximation avril 2024
current_time = int(time.time())

# CLTV : ne peut pas etre depense avant le bloc 850,000
cltv_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 confirmation
csv_blocks = 144  # ~24 heures
csv_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_block
print(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"))
--- 4.2 Timelock (CLTV / CSV) ---

CLTV (CheckLockTimeVerify) - Timelock absolu :
  Bloc actuel      : 840,000
  Bloc cible        : 850,000
  Blocs restants    : 10,000 (~1666 heures)
  Status            : VERROUILLE

CSV (CheckSequenceVerify) - Timelock relatif :
  Delai requis      : 144 blocs (~24 heures)
  Usage typique     : delai de securite apres depot

Comparaison des timelocks :
  Type       Reference            Usage courant
  CLTV       Bloc ou timestamp    Heritage, paris, echeances
  CSV        Blocs relatifs       Lightning Network, securite
  nLocktime  Champ de la tx       Transactions differees

Interpretation : Verrous temporels (Timelocks)

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 hash

redeem_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 simulees
alice_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

  1. Securite par restriction : impossible de créer de bugs complexes
  2. Verification rapide : l’exécution se termine toujours en temps borne
  3. Confidentialite : le script n’est revele qu’a la depense (P2SH)
  4. Previsibilite des frais : proportionnels a la taille, pas a la complexite

Avantages d’Ethereum/Solidity

  1. Expressivite : tout algorithme peut etre encode
  2. Etat persistent : le contrat “se souvient” entre les appels
  3. Composabilite : les contrats peuvent s’appeler entre eux (DeFi)
  4. Ecosysteme : outils, librairies, standards (ERC-20, ERC-721)

Convergence recente

  • 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 hashlib
import time


def sign(pubkey):
    """Generer une signature simulee a partir d'une cle publique."""
    if isinstance(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_pubkey
        self.bob = bob_pubkey
        self.carol = carol_pubkey
        self.amount = amount
        self.timeout_blocks = timeout_blocks
        self.creation_block = 0
        self.is_spent = False

    def 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 in zip(signatures, signers):
            if signer in keys and signer not in seen and valid_signature(sig, signer):
                valid += 1
                seen.add(signer)
        return valid >= 2

    def spend_multisig(self, sig_a, signer_a, sig_b, signer_b):
        """Depenser via la branche multisig (2-of-3)."""
        if self.is_spent:
            raise RuntimeError("Escrow deja depense")
        if not self._check_multisig([sig_a, sig_b], [signer_a, signer_b]):
            raise RuntimeError("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 = True
        return recipient

    def spend_timeout(self, sig_bob, current_block):
        """Depenser via la branche timeout (Bob seul apres N blocs)."""
        if self.is_spent:
            raise RuntimeError("Escrow deja depense")
        if current_block < self.creation_block + self.timeout_blocks:
            raise RuntimeError("Timeout non atteint")
        if not valid_signature(sig_bob, self.bob):
            raise RuntimeError("Signature de Bob invalide")
        self.is_spent = True
        return "Bob"


# --- Tests des 3 scenarios ---
print("TIMELOCK ESCROW - Test des 3 scenarios")
print("=" * 60)

# Generation des cles
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]

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 = 100
sig_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_pubkey
        self.bob = bob_pubkey
        self.carol = carol_pubkey
        self.amount = amount
        self.timeout_blocks = timeout_blocks
        self.grace_period = grace_period
        self.creation_block = 0
        self.is_spent = False

    def 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
        """
        if self.is_spent:
            raise RuntimeError("Escrow deja depense")
        if current_block < self.creation_block + self.timeout_blocks:
            raise RuntimeError(
                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:
            raise RuntimeError(
                f"Delai de grace expire au bloc {self.creation_block + self.timeout_blocks + self.grace_period}"
            )
        if not valid_signature(sig_bob, self.bob):
            raise RuntimeError("Signature de Bob invalide")
        self.is_spent = True
        return "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
        """
        if self.is_spent:
            raise RuntimeError("Escrow deja depense")
        if current_block < self.creation_block + self.timeout_blocks + self.grace_period:
            raise RuntimeError(
                f"Delai de grace pas encore expire : bloc {current_block} < {self.creation_block + self.timeout_blocks + self.grace_period}"
            )
        if not valid_signature(sig_alice, self.alice):
            raise RuntimeError("Signature d'Alice invalide")
        self.is_spent = True
        return "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 = 0
recipient1 = 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 = 0
recipient2 = 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) -> rejet
print("--- Scenario 3 : Bob arrive apres le delai de grace (rejet attendu) ---")
e3 = GracePeriodEscrow(alice_key, bob_key, carol_key, amount=5)
e3.creation_block = 0
try:
    e3.spend_grace_timeout(sign(bob_key), current_block=1300)
    print("ERREUR : aurait du etre rejete")
except RuntimeError as err:
    print(f"Rejet correct : {err}")
print()

# Scenario 4 : Bob essaie avant le timeout -> rejet
print("--- Scenario 4 : Bob essaie avant le timeout (rejet attendu) ---")
e4 = GracePeriodEscrow(alice_key, bob_key, carol_key, amount=5)
e4.creation_block = 0
try:
    e4.spend_grace_timeout(sign(bob_key), current_block=500)
    print("ERREUR : aurait du etre rejete")
except RuntimeError as 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

Notebook suivant : SC-21-Move-Sui-Python - Le langage Move et la blockchain Sui

Resume et perspectives

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.


<< Ripple XRP | Move & Sui >>

Retour au sommet