Chapitre 8 — Fondements mathématiques avancés de la sécurité blockchain

Comprendre les mécanismes cryptographiques avancée qui sous-tendent la sécurité des blockchains modernes : des structures algébriques aux preuves zero-knowledge.

I

Fondements mathématiques avancés de la sécurité blockchain

Explorez les fondations cryptographiques qui garantissent la sécurité des protocoles blockchain.

A

Structures algébriques et cryptographie moderne

1

Groupes, anneaux, corps finis \(\mathbb{F}_p\)

Groupe Abélien Corps fini Modulo p Inverse multiplicatif
Définition d'un corps fini
\[\mathbb{F}_p = \{0, 1, 2, \ldots, p-1\} \quad \text{avec} \quad p \text{ premier}\]

On travaille en cryptographie « classique blockchain » sur des structures algébriques où l'opération est fermée, associative, admet un neutre et des inverses. Un groupe est l'objet minimal ; dans la plupart des protocoles de signature, on vise un groupe abélien (commutatif). Un corps fini (avec premier) est muni de \(+\) et \(\times\) modulo \(p\). Toute division par \(a\) est tel que \(a \cdot a^{-1} \equiv 1 \pmod{p}\).

Prenons un exempele simple : Si il est 22h et que j'ajoute 4 heures, quelle heure est-il ? Réponse : 2h. Pas 26h. Vous venez de faire 22 + 4 = 2 dans un espace modulo 12. Les nombres ne dépassent pas 12, ils repartent de 0. En cryptographie, on fait la même chose, mais avec des nombres premiers gigantesques à la place de 12.

Résumé

La sécurité ECC se formule dans des groupes (souvent abéliens) définis sur des corps finis \(\mathbb{F}_p\).

2

Problème du logarithme discret (DLP) et complexité

DLP ECDLP Pollard rho Complexité Paramètres
Problème du logarithme discret
\[\text{DLP: } g^x \equiv h \pmod{p} \quad \Rightarrow \quad x = \log_g(h)\]

Dans \(\mathbb{F}_p^*\), le DLP est : étant donné \(g\) (générateur) et \(h\), retrouver \(x\). Sur courbes elliptiques, l'analogue est l'ECDLP : étant donné \(G\) et \(P\), retrouver \(k\). La différence structurante : en \(\mathbb{F}_p^*\), il existe des algorithmes sub-exponentiels (type index calculus) ; pour ECDLP sur courbes bien choisies, les meilleures attaques génériques restent de type \(O(\sqrt{n})\) (Pollard rho).

Imaginez un annuaire téléphonique. Trouver le numéro d'une personne quand on connaît son nom : facile, c'est trié. Trouver le nom d'une personne à partir de son numéro : il faut tout parcourir.Ainsi g^k permet de trouver un numéro depuis un nom. A l'inverse, retrouver k depuis h, c'est chercher un nom depuis un numéro dans un annuaire de 2^256 entrées

Résumé

ECDLP est le cœur de la sécurité ECC ; ses meilleures attaques connues sont génériques en \(O(\sqrt{n})\), d'où des clés compactes.

3

Hypothèses de sécurité et réduction

Hypothèse Réduction Oracle aléatoire Nonce Side-channel
Réduction de sécurité
\[\text{Si } \mathcal{A}_{\text{forger}} \Rightarrow \mathcal{B}_{\text{solver ECDLP}}\]

Une blockchain ne « prouve » pas la sécurité : elle assume des problèmes difficiles et construit des schémas qui s'y réduisent. Typiquement : si un attaquant forge une signature, alors il résout ECDLP (ou une hypothèse proche), ce qui est réputé infaisable. Il faut distinguer : hypothèse mathématique (dureté ECDLP), hypothèse modèle (oracle aléatoire), hypothèse implémentation (génération d'aléa, nonces, side-channels).

Résumé

La sécurité est une chaîne : hypothèse mathématique + modèle + implémentation ; l'implémentation casse souvent la théorie.

B

Courbes elliptiques et signatures numériques

1

Courbe elliptique : définition et loi de groupe

Courbe elliptique Point à l'infini Loi de groupe Scalaire Double-and-add
Équation de Weierstrass
\[y^2 = x^3 + ax + b \quad \text{sur } \mathbb{F}_p\]

Une courbe elliptique (sur \(\mathbb{F}_p\)) est typiquement \(y^2 = x^3 + ax + b\) avec \(4a^3 + 27b^2 \neq 0\) (non-singularité). Les points plus un point à l'infini \(O\) forment un groupe. L'addition est définie géométriquement (sécante / tangente) puis « re-projetée » dans \(\mathbb{F}_p\). La multiplication scalaire (k fois) se calcule via double-and-add, wNAF, etc.

Une courbe elliptique, c'est une équation du type y² = x³ + ax + b. Rien de mystérieux. Ce qui est remarquable, c'est que les points de cette courbe peuvent être 'additionnés' entre eux selon une règle géométrique précise. Pour additionner deux points A et B : on trace la droite qui passe par A et B. Cette droite coupe la courbe en un troisième point. On prend son symétrique par rapport à l'axe horizontal. Ce symétrique, c'est A + B. Et la multiplication scalaire k·G : c'est additionner G avec lui-même k fois. G + G + G... k fois.
Votre clé privée, c'est k. Votre clé publique, c'est le point Q = k·G. G est un paramètre public de la courbe, le même pour tout le monde sur Bitcoin ou Ethereum. Calculer Q depuis k : rapide, quelques millisecondes. Retrouver k depuis Q : c'est ECDLP. Infaisable.

Résumé

ECC repose sur le groupe des points d'une courbe elliptique sur \(\mathbb{F}_p\), où la clé publique est \(P = k \cdot G\).

Visualisation interactive — Multiplication scalaire k·G sur courbe elliptique réelle
-1
1
y² = x³ 1·x + 1  |  Δ = −4
DLP vs ECDLP
DLP dans ℤ/pℤ

Trouver k tel que :

gᵏ ≡ h (mod p)
Index Calculus Sub-exp
ECDLP sur E(𝔽ₚ)

Trouver k tel que :

k·G = Q
Pollard ρ O(√p)
Multiplication k·G
Valeur de k : 3
Courbes notables
P-256 a = −3, b = 0
Weierstrass a = −1, b = 1
secp256k1 a = 0, b = −4

La multiplication scalaire k·G est rapide (double-and-add), mais retrouver k depuis Q = k·G reste infaisable sans structure algébrique exploitable.

2

ECDSA : logique interne et point critique (nonce)

ECDSA Nonce k Inverse modulaire r, s RFC6979
Signature ECDSA
\[r = x_1 \mod n, \quad s = k^{-1}(h + d \cdot r) \mod n\]

ECDSA (schéma) : on a une courbe, un point générateur \(G\) d'ordre \(n\), une clé privée \(d\), clé publique \(Q = d \cdot G\). Signature d'un message \(m\) : on calcule \(e = H(m)\). On choisit un nonce aléatoire \(k\). On calcule \(r\), puis \(s = k^{-1}(e + d \cdot r) \mod n\). La signature est \((r, s)\). Le point critique : si \(k\) est réutilisé ou prévisible, on remonte \(d\) (clé privée) algébriquement.

On a une courbe, un point générateur G public, une clé privée d, une clé publique Q = d·G. Pour signer un message m, on fait trois choses. Un : on calcule le hash du message — H(m). Deux : on choisit un entier k aléatoire — le nonce. Trois : on calcule r et s selon les formules du protocole. La signature, c'est la paire (r, s). Pour vérifier : n'importe qui avec la clé publique Q peut recalculer et vérifier que (r, s) correspond bien à ce message signé par le détenteur de d. Sans jamais voir d. Le nonce k doit être absolument unique et imprévisible. À chaque signature. Pourquoi ? Si vous utilisez le même k deux fois pour deux messages différents, les formules créent deux équations avec deux inconnues connues de l'attaquant. Il peut calculer k. Et depuis k et la signature, il remonte à d, votre clé privée. Tout est compromis.

Résumé

ECDSA est sûr si et seulement si le nonce est unique, imprévisible et bien géré.

3

EdDSA / Ed25519 : raison d'être et usage

EdDSA Ed25519 Courbe Edwards Encodage Robustesse
Courbe d'Edwards
\[x^2 + y^2 = 1 + d x^2 y^2 \quad \text{sur } \mathbb{F}_p\]

EdDSA (et Ed25519) vise une signature plus « construite » : courbe Edwards ( Curve25519), encodage canonique, et surtout génération de nonce via hachage interne, réduisant les erreurs d'implémentation. Ed25519 travaille sur un groupe d'ordre grand premier (ou cofacteur géré), et normalise les opérations ; c'est attractif pour des systèmes où la surface d'attaque « implémentation » est un sujet majeur.

Résumé

Ed25519 est un choix « ingénierie sécurité » : signatures rapides, encodage strict, moins de pièges que ECDSA mal implémenté.

Comparaison ECDSA vs EdDSA — Processus de signature & points critiques
ECDSA
secp256k1 · Bitcoin, Ethereum
EdDSA / Ed25519
Curve25519 · Solana, Cardano, SSH
Setup — Clés
clé privée : d ∈ [1, n−1]
clé publique : Q = d·G
Courbe E sur 𝔽ₚ, générateur G d'ordre n.
Nonce aléatoire ⚠️
k ← aléatoire ∈ [1, n−1]
(x₁, y₁) = k·G
Point critique : k doit être unique et imprévisible à chaque signature.
Hash du message
e = H(m)
SHA-256 ou autre hash approuvé.
Calcul de la signature
r = x₁ mod n
s = k⁻¹(e + d·r) mod n
signature : (r, s)
Inverse modulaire de k requis.
Vérification
u₁ = e·s⁻¹ mod n
u₂ = r·s⁻¹ mod n
vérif : (u₁·G + u₂·Q).x ≡ r
🚨
Si k réutilisé : deux signatures (r,s₁) et (r,s₂) permettent de calculer k puis de remonter d algébriquement. Attaque Sony PS3 (2010), Mt. Gox…
Setup — Clés
seed : k ∈ {0,1}²⁵⁶
(a, prefix) = H(k)
clé publique : A = a·B
Courbe Edwards Curve25519, générateur B d'ordre premier ℓ.
Nonce déterministe 🔒
r = H(prefix ‖ m) mod ℓ
R = r·B
Sécurité par construction : r dérivé par hash — jamais aléatoire externe, jamais réutilisable.
Hash global
h = H(R ‖ A ‖ m) mod ℓ
R, la clé publique et le message hashés ensemble — lie la signature au contexte complet.
Calcul de la signature
S = (r + h·a) mod ℓ
signature : (R, S)
Pas d'inverse modulaire requis — plus simple et plus rapide.
Vérification
h = H(R ‖ A ‖ m)
vérif : S·B = R + h·A
Encodage canonique — vérification déterministe, sans ambiguïté.
Nonce par hachage : impossible à réutiliser par construction. Encodage strict élimine la malléabilité. Surface d'implémentation réduite.
ECDSA — Points de vigilance
Nonce k externe — nécessite un RNG de qualité
Réutilisation de k → exposition totale de d
Malléabilité : (r, s) et (r, −s) valides
RFC 6979 : déterminisme optionnel (recommandé)
Utilisé par Bitcoin (secp256k1), Ethereum
EdDSA / Ed25519 — Avantages
Nonce déterministe par construction
Encodage canonique — pas de malléabilité
Pas d'inverse modulaire → vérification plus rapide
Surface d'implémentation réduite
Utilisé par Solana, Cardano, SSH, TLS 1.3
C

Fonctions de hachage, Merkle et preuves

1

Propriétés cryptographiques d'un hash

Préimage Seconde préimage Collision Birthday bound Engagement
Résistance aux collisions
\[\text{Birthday bound: } 2^{n/2} \text{ pour n bits de sortie}\]

Un hash n'est pas « juste » une empreinte : la sécurité dépend de trois propriétés : résistance à la préimage : donné \(y\), trouver \(x\) tel que \(H(x) = y\) est difficile ; résistance à la seconde préimage : donné \(x\), trouver \(x' \neq x\) tel que \(H(x') = H(x)\) est difficile ; résistance aux collisions : trouver \(x, x'\) tels que \(H(x) = H(x')\) est difficile (attaque birthday pour n bits).

Résistance aux collisions — Propriétés cryptographiques & Birthday Bound
① Préimage
Résistance à la préimage
Donné y, trouver x
tel que H(x) = y
→ infaisable
Impossible de remonter à l'entrée depuis le hash. Fondement de l'intégrité.
Coût attaque : O(2ⁿ)
② 2ᵉ Préimage
Résistance à la 2ᵉ préimage
Donné x, trouver x'≠x
tel que H(x) = H(x')
→ infaisable
Impossible de substituer un message par un autre avec le même hash.
Coût attaque : O(2ⁿ)
③ Collision
Résistance aux collisions
Trouver x ≠ x'
tels que H(x) = H(x')
→ infaisable
Aucun attaquant ne peut trouver deux entrées distinctes avec le même digest.
Birthday bound : O(2ⁿ/²)
Birthday Bound — Comparaison des niveaux de sécurité selon n
SHA-1 (160b)
collision
collision : 2⁸⁰
SHA-256 (256b)
collision
collision : 2¹²⁸
SHA-512 (512b)
collision
collision : 2²⁵⁶
Birthday bound : pour un hash de n bits, une attaque par anniversaire trouve une collision en ~2n/2 essais — deux fois moins que l'espace total 2n. SHA-256 offre donc 128 bits de sécurité effective contre les collisions, suffisant pour la blockchain.
Résumé

La blockchain s'appuie sur préimage / seconde préimage / collision, et sur les ordres de grandeur de complexité associés.

2

Arbres de Merkle : preuve d'inclusion et complexité

Merkle tree Preuve d'inclusion Racine Log N Client léger
Racine de Merkle
\[H_{root} = H(H(A) || H(B)) || H(H(C) || H(D))\]

On a une liste de feuilles (souvent des hashes de transactions). On construit des nœuds parents par concaténation et hachage jusqu'à la racine \(MerkleRoot\). Une preuve d'inclusion d'une feuille est la liste des « sibling hashes » sur le chemin vers la racine. Vérification : on recompute itérativement et on compare au Merkle root du bloc. Complexité : preuve en \(O(\log N)\), taille \(O(\log N)\), ce qui permet des vérifications légères (SPV / light clients).

Section III · A · 1 — Principe et racine

L'arbre de Merkle

Organisation hiérarchique des hachages · Une racine suffit à vérifier l'intégrité de l'ensemble

HABCD = H( HAB ‖ HCD )
Racine de Merkle
HAB = H( HA ‖ HB )
Nœud interne
HCD = H( HC ‖ HD )
Nœud interne
HA = H(TxA)
Feuille A
HB = H(TxB)
Feuille B
HC = H(TxC)
Feuille C
HD = H(TxD)
Feuille D
📄
Transaction A
📄
Transaction B
📄
Transaction C
📄
Transaction D
Racine — empreinte globale du bloc
Nœuds internes — agrégation de hachages
Feuilles A–B — transactions vérifiées
Feuilles C–D — transactions vérifiées

🔍 Preuve de Merkle — vérifier TxB sans révéler tout le bloc

01 Le vérificateur possède uniquement HA et HCD — deux hachages fournis par un nœud.
02 Il calcule HAB = H(HA ‖ HB), puis HABCD = H(HAB ‖ HCD).
03 Si HABCD correspond à la racine inscrite dans l'en-tête du bloc, TxB est prouvée valide — sans accéder à TxA, TxC ni TxD.
Propriété fondamentale
Modifier TxB ↔ change HB
↓ propage à HAB
↓ propage à HABCD (racine)
→ falsification immédiatement détectable
Efficacité de la preuve
N transactions → log₂(N) hachages suffisent
Ex : 1 024 tx → preuve en 10 hachages
Ex : 1 M tx   → preuve en 20 hachages
Usage dans Bitcoin
Racine stockée dans l'en-tête du bloc
Nœuds légers (SPV) vérifient
sans télécharger le bloc entier
→ confiance fractionnable
H( · ) désigne une fonction de hachage cryptographique · ‖ désigne la concaténation
Résumé

Merkle permet de prouver l'appartenance à un ensemble sans révéler l'ensemble, avec preuve logarithmique.

3

ZK (cadre formel) : completeness / soundness / zero-knowledge

Zero-knowledge Completeness Soundness Preuve Confidentialité
Propriétés ZK — Système de preuve interactif ⟨P, V⟩
① Completeness
Complétude
Si x ∈ L (vrai) :
Pr[⟨P, V⟩ = 1] = 1
— P honnête ⟹ V accepte
Un prouveur honnête qui connaît le témoin w convainc toujours le vérificateur.
Pr[accept] = 1
② Soundness
Solidité
Si x ∉ L (faux) :
Pr[P* ⟹ V accepte] ≤ ε
— tricheur ⟹ V rejette
Un prouveur malhonnête P* ne peut convaincre V qu'avec probabilité négligeable ε.
Pr[accept] ≤ ε ≈ 0
③ Zero-Knowledge
Non-divulgation
∃ simulateur S :
ViewV(P,V) S(x)
— V n'apprend rien sur w
V apprend uniquement que l'énoncé est vrai — le témoin secret w reste totalement caché.
🔒
w reste secret

Un système ZK se juge par 3 propriétés : Completeness : un prouveur honnête convince un vérificateur honnête ; Soundness : un tricheur ne convainc pas (sauf probabilité négligeable) ; Zero-knowledge : le vérificateur n'apprend rien au-delà de la validité. Dans une perspective blockchain : ZK sert soit à scaler ( preuves d'exécution), soit à confidentialiser ( preuve sans révélation), soit aux deux.

Résumé

ZK formalise « prouver sans révéler » avec des garanties mesurables : complétude, solidité, et non-divulgation.

Exercice fil rouge — Partie 1

Audit mathématique et cryptographique

Contexte général

Une organisation internationale souhaite créer une DAO académique permettant : le financement de projets de recherche, le vote décentralisé sur les allocations budgétaires, la transparence des décisions, la vérifiabilité mathématique des signatures.

Objectif

Évaluer la solidité mathématique des primitives utilisées par la DAO.

Travail demandé

  • 1. Cryptographie asymétrique : Expliquer formellement un groupe fini, un corps fini, une loi de groupe sur une courbe elliptique. Justifier pourquoi ECDLP est considéré comme difficile.
  • 2. Sécurité des signatures : Expliquer mathématiquement les étapes de génération de clé, signature, vérification ECDSA. Comparer ECDSA et Ed25519.
  • 3. Intégrité des votes : Construire formellement un arbre de Merkle à partir de 8 votes. Décrire une preuve d'inclusion.
II

Lecture technique comparative des blockchains

Analyse comparative des architectures blockchain à travers leur code source.

A

Bitcoin — C++ : validation, UTXO, Script

1

Validation PoW : du « nBits » au target

nBits Target SetCompact Proof-of-Work

Bitcoin encode la difficulté via un champ compact « nBits » (format compact) qui se convertit en target 256 bits. Le cœur du contrôle n'est pas « mystique » : on convertit, puis on compare le hash du bloc au target.

C++
bool CheckProofOfWork(uint256 hash, unsigned int nBits, ...);

// Conversion nBits vers target
Target.SetCompact(nBits, &fNegative, &fOverflow);

// La preuve est valide si hash <= target
bool isValid = hash <= target;
Résumé

La PoW Bitcoin est un test d'ordre entre hash et target dérivé de nBits.

2

Modèle UTXO : CTxIn/CTxOut

UTXO vin/vout OutPoint Dépense

Bitcoin ne suit pas un solde de compte ; il suit des sorties non dépensées (UTXO : Output Transaction Unspent). Une transaction consomme des sorties précédentes via des inputs qui pointent vers un outpoint (txid, index).

C++
// Structure d'une transaction Bitcoin
class CTransaction {
public:
    std::vector vin;   // Inputs: références aux UTXO précédents
    std::vector vout; // Outputs: nouveaux UTXO créés
};

// Input référence un UTXO précédent
class CTxIn {
    COutPoint prevout;  // (txid, index)
    CScript scriptSig;  // Script de déverrouillage
};
Résumé

UTXO = on dépense des sorties, pas un solde ; une tx est un graphe de références + nouveaux outputs.

3

Script : la validation comme machine à pile

Script Stack VM VerifyScript Conditions

Le Script Bitcoin est un langage à pile STACK-BASED VM — P2PKH ScriptSig + ScriptPubKey : <sig> <pubKey> OP_DUP OP_HASH160 OP_CHECKSIG ① PUSH <sig> <sig> — vide — pile ② PUSH <pubKey> <pubKey> <sig> pile ③ OP_CHECKSIG TRUE — vide — pile OPCODES CLÉS PUSH → empile une valeur sur la pile OP_DUP → duplique le sommet de pile OP_HASH160 → SHA256 puis RIPEMD160 OP_EQUALVERIFY → compare, revert si ≠ OP_CHECKSIG → vérifie la signature ECDSA ✓ Pile termine sur TRUE → transaction valide Non-Turing-complet : pas de boucles, pas d'état global, pas d'appels externes. , volontairement restreint. La validation s'opère via une fonction de vérification de script : on évalue un scriptSig + scriptPubKey (et parfois witness), sous des flags de validation.

C++
// Point d'entrée de validation de script
bool VerifyScript(
    const CScript& scriptSig,
    const CScript& scriptPubKey,
    const CTxOut& txout,
    unsigned int nIn,
    unsigned int flags,
    ...);

// Exemple: P2PKH (Pay to Public Key Hash)
// ScriptPubKey: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
// ScriptSig: <signature> <pubKey>
Résumé

Bitcoin Script valide des conditions de dépense ; c'est une VM à pile, pas une plateforme d'applications générales.

B

Ethereum — Geth : état, EVM, gas

1

Account model : l'État comme structure persistante

Account model StateDB Transition d'état Trie

Ethereum maintient un état global (balances, code, storage) indexé par adresses, mis à jour à chaque transaction. Le client (ex. Geth) utilise une couche StateDB qui gère cache, journaling, commits vers trie.

Go
// StateDB gère l'état Ethereum
type StateDB interface {
    // Lecture d'état
    GetBalance(addr common.Address) *big.Int
    GetCode(addr common.Address) []byte
    GetState(addr common.Address, key common.Hash) common.Hash
    
    // Écriture d'état
    SetBalance(addr common.Address, *big.Int)
    SetCode(addr common.Address, []byte)
    SetState(addr common.Address, key, value common.Hash)
    
    // Finalisation
    Commit(write bool) (root common.Hash, err error)
}
Résumé

Ethereum est une machine de transition d'état : la transaction transforme un état global géré par StateDB.

2

EVM : VM de bytecode et boucle d'exécution

EVM Bytecode Opcodes Stack machine

Geth expose explicitement que la VM exécute du bytecode selon les règles du protocole. L'EVM est une VM à pile (stack machine) qui lit opcodes, manipule stack/memory/storage, et applique les règles de gas.

Go
// Boucle d'exécution de l'EVM
func (evm *EVM) Run(pc *uint64, contract *Contract) ([]byte, error) {
    for {
        // Lecture de l'opcode à l'adresse PC
        op := OpCode(code[*pc])
        
        // Exécution de l'opcode
        operation := operations[op]
        if operation.execute == nil {
            return nil, fmt.Errorf("invalid opcode %v", op)
        }
        
        // Exécution avec coût gas
        evm.gas -= operation.gasCost(evm, args...)
        if evm.gas < 0 {
            return nil, ErrOutOfGas
        }
        
        // Exécution de l'opération
        operation.execute(&evm.stack, &evm.memory, &evm.storage)
        
        // Incrémentation du PC
        *pc++
    }
}
Résumé

L'EVM exécute du bytecode (opcodes) avec une comptabilité de ressources (gas) intégrée à l'exécution.

3

Gas : contrainte économique + sécurité d'exécution

Gas Anti-DoS Coût opcode Allocation

Le gas joue 3 rôles : mesure de coût : chaque opcode a un prix ; anti-DoS : une boucle infinie « brûle » du gas et s'arrête ; marché : prix du gas forme un mécanisme d'allocation de bloc.

Go
// Coûts de gas pour opérations courantes
const (
    GasQuickStep       uint64 = 2     // Opérations simples
    GasFastStep        uint64 = 3     // Opérations rapides
    GasMidStep         uint64 = 5     // Opérations moyennes
    GasExtStep         uint64 = 20    // Opérations extensives
    GasStorageSet      uint64 = 20000  // Écriture storage
    GasStorageUpdate   uint64 = 5000   // Mise à jour storage
    GasCall            uint64 = 700    // Appel de contrat
)

// Calcul du gas pour un appel de fonction
func gasCall(evm *EVM, contract *Contract, args ...interface{}) (uint64, error) {
    gas := GasCall
    if len(args) > 0 {
        // Ajouter gas pour la mémoire
        gas += memoryGasCost(args)
    }
    return gas, nil
}
Résumé

Le gas est une contrainte de calcul, une défense anti-DoS et un mécanisme de marché.

C

Monero — confidentialité : ring signatures, RingCT, stealth

1

Ring signatures : anonymat d'entrée

Ring signature Decoys Anonymity set Unlinkability

Une ring signature permet de prouver qu'un membre d'un ensemble de clés a signé, sans révéler lequel. Dans Monero, cela sert à masquer quel input est le vrai parmi des leurres (decoys). Définition : « computationally infeasible to determine which key was used ».

C++
// Concept de ring signature (pseudo-code)
class RingSignature {
    // Génération de la signature
    static bool generate(
        const std::vector<const crypto::public_key>& ringKeys,  // Ensemble de clés
        const crypto::secret_key& secretKey,                        // Clé réelle
        const crypto::hash& message,
        crypto::signature& sig
    ) {
        // 1. Sélectionner un indice aléatoire pour la vraie clé
        size_t realIndex = random(0, ringKeys.size());
        
        // 2. Générer des leurres (decoys) pour les autres indices
        for (size_t i = 0; i < ringKeys.size(); i++) {
            if (i != realIndex) {
                ringKeys[i] = generateDecoyKey();
            }
        }
        
        // 3. Créer la signature qui preuve possession de realIndex
        return createMLSAG(ringKeys, secretKey, message, sig);
    }
    
    // Vérification: peut être faite sans savoir quelle clé a signé
    static bool verify(
        const std::vector<const crypto::public_key>& ringKeys,
        const crypto::hash& message,
        const crypto::signature& sig
    ) {
        return checkMLSAG(ringKeys, message, sig);
    }
};
Résumé

Ring signatures masquent l'auteur réel d'une dépense parmi un ensemble de signataires possibles.

2

RingCT : masquage des montants

RingCT Confidential Transactions Commitments Preuve

RingCT (Ring Confidential Transactions) cache les montants via des engagements (commitments) et des preuves associées. L'idée : valider la conservation de valeur sans révéler les valeurs.

C++
// Concept de RingCT (pseudo-code)
class RingCT {
    // Engagement de Pedersen: C = x*G + v*H
    // x = blinding factor (secret), v = montant, G,H = generators
    static commitment createCommitment(const uint64_t amount) {
        scalar x = randomScalar();  // Facteur aveuglant
        point C = x*G + amount*H;   // Engagement
        return {C, x};              // Retourne engagement + facteur secret
    }
    
    // Vérification de conservation de valeur
    static bool verifyBalance(
        const std::vector<commitment>& inputCommits,
        const std::vector<commitment>& outputCommits,
        const std::vector<bulletproof>& proofs
    ) {
        // Σ(inputs) - Σ(outputs) = 0 (en commitment)
        point sumInputs = sum(inputCommits);
        point sumOutputs = sum(outputCommits);
        
        // Vérifier que la différence est zéro
        // en utilisant les preuves bulletproof
        for (const auto& proof : proofs) {
            if (!verifyBulletproof(proof, sumInputs - sumOutputs)) {
                return false;
            }
        }
        return true;
    }
};
Résumé

RingCT valide les sommes sans exposer les montants grâce à engagements et preuves.

3

Stealth addresses : masquer le destinataire

Stealth address One-time address Output Unlinkability

Les stealth addresses génèrent, à partir d'informations publiques, des adresses « à usage unique » (one-time) rendant difficile l'association d'un output à un destinataire public. L'objectif global : ring signatures → masque l'émetteur, stealth addresses → masque le destinataire, RingCT → masque le montant.

C++
// Concept de Stealth Address (pseudo-code)
class StealthAddress {
    // Génération d'adresse stealth à partir de la clé publique du destinataire
    static address generateStealth(
        const public_key& destPublicViewKey,   // Clé de vue publique
        const public_key& destPublicSpendKey,  // Clé de dépense publique
        const scalar& randomEntropy            // Random
    ) {
        // Calcul: P = Hs(r*A)*G + B
        // r = randomEntropy, A = clé de vue, B = clé de dépense
        scalar r = randomEntropy;
        point R = r * G;                              // Publication
        scalar Hs_rA = hashToScalar(r * destPublicViewKey);  // H(r*A)
        point P = Hs_rA * G + destPublicSpendKey;    // Adresse stealth
        
        return {P, R};  // R publié, P utilisé comme adresse
    }
    
    // Dépense: le destinataire calcule la clé privée correspondante
    static scalar derivePrivateKey(
        const scalar& r,                              // Secret utilisé
        const scalar& privateViewKey,                 // Clé de vue privée
        const scalar& privateSpendKey                 // Clé de dépense privée
    ) {
        scalar Hs_rA = hashToScalar(r * (privateViewKey * G));
        return Hs_rA + privateSpendKey;  // Clé privée de l'adresse stealth
    }
};
Résumé

Les stealth addresses rendent le lien output→destinataire difficile, via des adresses à usage unique.

Exercice fil rouge — Partie 2

Lecture comparative d'architectures

Objectif

Comprendre les implications techniques des différents modèles blockchain.

Travail demandé

  • 1. Bitcoin : Identifier le hash précédent, le nonce, la racine de Merkle. Expliquer le PoW. Décrire le modèle UTXO.
  • 2. Ethereum : Expliquer le modèle account-based. Décrire le rôle de l'EVM. Justifier le mécanisme du gas.
  • 3. Monero : Expliquer le principe d'une ring signature. Décrire RingCT. Comparer avec Bitcoin/Ethereum.

Livrable

  • Synthèse comparative (2-3 pages) évaluant sécurité, confidentialité, gouvernance implicite, compromis techniques.
III

Solidity — Architecture Ethereum et développement de smart contracts

Maîtrisez le développement de smart contracts sur Ethereum.

A

Comprendre l'environnement Ethereum pour coder en Solidity

1

Ethereum Virtual Machine (EVM) et bytecode

EVM Bytecode Stack machine Gas Déterminisme

Solidity est un langage de haut niveau compilé en bytecode EVM. L'EVM est une machine virtuelle déterministe à pile (stack machine). Caractéristiques : Stack 1024 éléments max, Types 256 bits natifs (uint256), Gas pour chaque opcode, Exécution atomique.

Solidity
// Flux: Solidity -> Compilateur (solc) -> Bytecode -> EVM

// Exemple de code Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleStorage {
    uint256 public storedData;
    
    function set(uint256 x) public {
        storedData = x;
    }
}

// Bytecode compilé (conceptuel):
// PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x04 PUSH1 0x00 ...
Résumé

Solidity produit du bytecode exécuté par l'EVM, une machine à pile déterministe régulée par le gas.

2

Structure d'un smart contract

Contract State variable Storage Function Adresse

Un contrat Solidity est : un programme, déployé à une adresse, possédant un stockage persistant. Structure minimale avec pragma, contract, state variables, functions.

Solidity
// Structure minimale d'un smart contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Compteur {
    // State variable: stockage on-chain
    uint256 public count;
    address public owner;
    
    // Constructeur: appelé au déploiement
    constructor() {
        owner = msg.sender;
    }
    
    // Fonction: logique métier
    function increment() public {
        count += 1;
    }
    
    function decrement() public {
        require(count > 0, "Cannot go below zero");
        count -= 1;
    }
}
Résumé

Un smart contract est une structure persistante déployée à une adresse et manipulant un état on-chain.

3

Storage, memory, calldata

Storage Memory Calldata Gas cost Persistance

Solidity distingue : storage → données persistantes on-chain ; memory → données temporaires pendant l'exécution ; calldata → données d'entrée immuables. Comprendre cette distinction est fondamental pour optimiser le gas.

Solidity
// Différence storage / memory / calldata
pragma solidity ^0.8.0;

contract StorageExample {
    // Storage: données persistantes on-chain
    uint256[] public numbers;
    
    function addNumber(uint256 _value) public {
        // _value est en calldata (immuable, pas de copy)
        // On copie en memory pour manipulation
        uint256 temp = _value;
        
        // push ajoute au storage (persistant, coûteux)
        numbers.push(temp);
    }
    
    function getSum() public view returns (uint256) {
        // Memory: calcul temporaire
        uint256 sum = 0;
        for (uint256 i = 0; i < numbers.length; i++) {
            sum += numbers[i];
        }
        return sum;
    }
}
Résumé

Solidity distingue stockage persistant et mémoire temporaire, ce qui impacte coûts et sécurité.

B

Solidity pas à pas — premier contrat sérieux

1

Variables, types et visibilité

uint256 address mapping Visibility Public state

Types principaux : uint256, address, bool, mapping, struct. Visibilité : public, private, internal, external. Chaque appel externe est public sur la blockchain.

Solidity
// Types et visibilité en Solidity
pragma solidity ^0.8.0;

contract TypesExample {
    // Types de base
    bool public paused = false;
    address public owner;
    uint256 public count = 0;
    
    // Mapping: table associative
    mapping(address => uint256) public balances;
    mapping(address => bool) public isWhitelisted;
    
    // Struct: structure de données personnalisée
    struct Proposal {
        string description;
        uint256 votesFor;
        uint256 votesAgainst;
        bool executed;
        address proposer;
    }
    
    // Tableau de structs
    Proposal[] public proposals;
    
    // Visibilité: 
    // public: accessible partout (externally + internally)
    // private: seulement dans ce contrat
    // internal: dans ce contrat et ses héritiers
    // external: seulement depuis l'extérieur
Résumé

Les types et la visibilité déterminent la structure des données et l'exposition publique du contrat.

2

Modifiers et contrôle d'accès

Modifier require msg.sender Access control Sécurité

Solidity permet de définir des règles via des modifiers. Concept clé : la blockchain ne protège rien par défaut. La sécurité est écrite dans le code.

Solidity
// Modifiers et contrôle d'accès
pragma solidity ^0.8.0;

contract AccessControl {
    address public owner;
    
    // Modifier: condition avant exécution de la fonction
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _; // Continue l'exécution de la fonction
    }
    
    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }
    
    bool public paused = false;
    
    constructor() {
        owner = msg.sender;
    }
    
    // Utilisation des modifiers
    function pause() public onlyOwner {
        paused = true;
    }
    
    function unpause() public onlyOwner {
        paused = false;
    }
    
    // Fonction protégée par plusieurs modifiers
    function sensitiveOperation() 
        public 
        onlyOwner 
        whenNotPaused 
    {
        // Logique sensibles
    }
}
Résumé

Les modifiers implémentent des règles de sécurité explicites, notamment le contrôle d'accès.

3

Events et interaction

Event Log Emit Indexation Interface

Les events permettent d'émettre des logs. Ils ne stockent pas de données persistantes, sont indexés pour lecture externe, et servent à l'interface front-end.

Solidity
// Events et interaction
pragma solidity ^0.8.0;

contract EventExample {
    // Déclaration d'un event
    // indexed: permet de filtrer par cette valeur
    event ValueChanged(
        address indexed user, 
        uint256 oldValue, 
        uint256 newValue
    );
    
    event Transfer(
        address indexed from,
        address indexed to,
        uint256 amount
    );
    
    uint256 public value;
    
    function setValue(uint256 _newValue) public {
        uint256 oldValue = value;
        value = _newValue;
        
        // Émission de l'event
        emit ValueChanged(msg.sender, oldValue, _newValue);
    }
    
    // Event avec adress: plus courant
    mapping(address => uint256) public balances;
    
    function transfer(address to, uint256 amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        balances[to] += amount;
        
        emit Transfer(msg.sender, to, amount);
    }
}
Résumé

Les events servent à notifier l'extérieur sans modifier l'état persistant.

C

Exercice pratique — Mini DAO en Solidity

1

Version 1 — Vote simple

Vote Compteur Fonction État On-chain
Solidity
// Version 1: Vote simple - Compteur de votes on-chain
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vote {
    // State variables: stockage on-chain
    uint256 public yes;
    uint256 public no;
    
    // everyone can vote yes
    function voteYes() public {
        yes += 1;
    }
    
    // everyone can vote no
    function voteNo() public {
        no += 1;
    }
    
    // Obtenir le résultat
    function getResult() public view returns (string memory) {
        if (yes > no) {
            return "Approved";
        } else if (no > yes) {
            return "Rejected";
        } else {
            return "Tied";
        }
    }
}
Résumé

Première DAO minimaliste : un compteur de votes on-chain.

2

Version 2 — Anti double vote

Mapping require Double vote Adresse Gouvernance
Solidity
// Version 2: Anti double vote avec mapping
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VoteWithPrevention {
    uint256 public yes;
    uint256 public no;
    
    // Mapping: enregistrement des votants
    // address => bool: a déjà voted?
    mapping(address => bool) public hasVoted;
    
    // Enums: type personnalisé
    enum VoteOption { None, Yes, No }
    mapping(address => VoteOption) public userVote;
    
    function voteYes() public {
        // Vérification: pas encore voted
        require(!hasVoted[msg.sender], "Already voted");
        
        // Enregistrement
        hasVoted[msg.sender] = true;
        userVote[msg.sender] = VoteOption.Yes;
        
        yes += 1;
    }
    
    function voteNo() public {
        require(!hasVoted[msg.sender], "Already voted");
        
        hasVoted[msg.sender] = true;
        userVote[msg.sender] = VoteOption.No;
        
        no += 1;
    }
}
Résumé

On empêche le double vote en stockant l'état des votants.

3

Version 3 — Mini DAO structurée

DAO Struct Gouvernance Exécution Règle codée
Solidity
// Version 3: Mini DAO structurée
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MiniDAO {
    // Struct: proposition avec état complet
    struct Proposal {
        string description;
        uint256 yesVotes;
        uint256 noVotes;
        bool executed;
        uint256 createdAt;
        address proposer;
    }
    
    // Storage
    Proposal[] public proposals;
    mapping(uint256 => mapping(address => bool)) public hasVoted;
    address public owner;
    
    // Événements
    event ProposalCreated(uint256 indexed id, string description);
    event VoteCast(uint256 indexed id, address voter, bool support);
    event ProposalExecuted(uint256 indexed id, bool approved);
    
    constructor() {
        owner = msg.sender;
    }
    
    // Créer une proposition
    function createProposal(string memory _description) public {
        proposals.push(Proposal({
            description: _description,
            yesVotes: 0,
            noVotes: 0,
            executed: false,
            createdAt: block.timestamp,
            proposer: msg.sender
        }));
        emit ProposalCreated(proposals.length - 1, _description);
    }
    
    // Voter sur une proposition
    function vote(uint256 _proposalId, bool _support) public {
        Proposal storage proposal = proposals[_proposalId];
        
        require(!proposal.executed, "Already executed");
        require(!hasVoted[_proposalId][msg.sender], "Already voted");
        
        hasVoted[_proposalId][msg.sender] = true;
        
        if (_support) {
            proposal.yesVotes++;
        } else {
            proposal.noVotes++;
        }
        
        emit VoteCast(_proposalId, msg.sender, _support);
    }
    
    // Exécuter une proposition (si majorité)
    function executeProposal(uint256 _proposalId) public {
        Proposal storage proposal = proposals[_proposalId];
        
        require(!proposal.executed, "Already executed");
        
        // Règle de gouvernance: > 50% et au moins 3 votes
        uint256 total = proposal.yesVotes + proposal.noVotes;
        require(total >= 3, "Quorum not reached");
        require(proposal.yesVotes > proposal.noVotes, "Not approved");
        
        proposal.executed = true;
        
        emit ProposalExecuted(_proposalId, true);
        // Logique d'exécution ici (transfert, etc.)
    }
}
Résumé

Une DAO est un ensemble de règles codées gouvernant des décisions collectives on-chain.

Exercice fil rouge — Partie 3

Implémentation d'une mini DAO en Solidity

Objectif

Coder un prototype fonctionnel.

Étape 0 — Prendre en main l'environnement remix.ethereum

  • Comprendre les fondements de cet environnement.

Étape 1 — Smart contract de base

  • Implémenter : une variable owner, une structure Proposal, un mapping hasVoted, une fonction createProposal().

Étape 2 — Système de vote sécurisé

  • Ajouter : fonction vote(uint proposalId), contrôle anti double vote, limite temporelle (block.timestamp), condition de quorum.

Étape 3 — Exécution conditionnelle

  • Implémenter : fonction executeProposal(), vérification majorité,'émission d'un event, impossibilité d'exécution multiple.

Contraintes techniques

  • Utiliser require. Séparer storage et memory. Documenter le code. Compiler sans warning. Tester au moins un cas d'erreur.
Référence — 10 erreurs fréquentes sur Remix & Solidity
❌ Cause

Solidity est strict sur la syntaxe. Un seul point-virgule oublié ou une accolade non fermée bloque toute la compilation.

// ❌ Oubli du ; uint256 public count = 0 // ❌ Accolade manquante function reset() public { count = 0; // ← accolade fermante oubliée
✓ Solution

Le numéro de ligne dans le message d'erreur indique l'endroit exact. Vérifiez cette ligne et la précédente. Raccourci Remix : Alt+Shift+F pour reformater.

// ✓ Correct uint256 public count = 0; function reset() public { count = 0; } // ← accolade fermante
💡 Astuce : Remix colore les accolades en paires. Cliquez sur l'une d'elles : son homologue se surligne en jaune.
❌ Cause

Les types complexes (string, bytes, tableaux, structs) dans les paramètres de fonctions exigent une localisation mémoire explicite.

// ❌ Manque memory function setMsg(string _msg) public { message = _msg; }
✓ Solution

Ajoutez memory pour les paramètres de fonctions. Utilisez storage uniquement pour les références à des variables d'état.

// ✓ Paramètre de fonction function setMsg(string memory _msg) public { message = _msg; } // ✓ Référence à variable d'état MyStruct storage s = structs[id];
💡 Règle simple : paramètre de fonction → memory. Référence à une variable du contrat → storage.
❌ Cause

La version sélectionnée dans le compilateur Remix ne correspond pas à la directive pragma de votre fichier.

// Votre fichier déclare : pragma solidity ^0.8.0; // Remix compile avec 0.6.x ⚠ Version mismatch
✓ Solution

Dans l'onglet Solidity Compiler, ouvrez le sélecteur de version et choisissez une version 0.8.x. Activez "Auto compile".

// ✓ Votre fichier pragma solidity ^0.8.0; // ✓ Remix réglé sur 0.8.20 (toute 0.8.x)
💡 Bonne pratique : utilisez toujours ^0.8.0 — le ^ signifie « compatible avec toute la série 0.8.x ».
❌ Cause

"Injected Web3" connecte Remix à votre wallet MetaMask et à une vraie blockchain. Chaque déploiement consomme du gas réel.

Deploy & Run → Environment : Injected Provider - MetaMask → MetaMask s'ouvre → Coût en ETH réel !
✓ Solution

Dans Deploy & Run → Environment, sélectionnez toujours "Remix VM (Shanghai)" pour ce cours. 100 ETH fictifs, aucun wallet.

Deploy & Run → Environment : Remix VM (Shanghai) → Blockchain simulée → 100 ETH fictifs, 0 risque
💡 Règle absolue : si une popup MetaMask s'ouvre, cliquez Annuler immédiatement et revenez à Remix VM.
❌ Cause

Vous utilisez une variable inexistante dans le scope courant : faute de frappe dans le nom, oubli de déclaration, ou utilisation hors scope.

// ❌ Faute de frappe uint256 public counter; function inc() public { conter++; // 'counter' mal écrit }
✓ Solution

Vérifiez l'orthographe exacte. Solidity est case-sensitive : Counter ≠ counter. Déclarez toutes les variables d'état en haut du contrat.

// ✓ Nom correct uint256 public counter; function inc() public { counter++; }
💡 Conseil : Remix propose de l'autocomplétion (Ctrl+Espace). Utilisez-la pour éviter les fautes de frappe.
❌ Cause

Sans require(), n'importe qui peut appeler vos fonctions avec n'importe quelle valeur. Le contrat compile mais son comportement est non sécurisé.

// ❌ Pas de protection function vote(uint id) public { votes[id]++; // id inexistant ? // double vote ? }
✓ Solution

Utilisez require(condition, "message") pour valider chaque précondition en début de fonction.

// ✓ Avec protections function vote(uint id) public { require(id < proposals.length, "Proposal inexistante"); require(!hasVoted[msg.sender], "Deja vote"); votes[id]++; }
💡 Bonne pratique : chaque require() est une ligne de défense. Écrivez-les avant toute modification d'état.
❌ Cause

Si vous envoyez de l'ETH avant de mettre à jour votre état, un contrat malveillant peut rappeler votre fonction en boucle avant que l'état soit mis à jour.

// ❌ Pattern dangereux function withdraw() public { uint amt = balances[msg.sender]; payable(msg.sender).transfer(amt); // ↑ ETH envoyé AVANT balances[msg.sender] = 0; // trop tard }
✓ Solution

Appliquez le pattern Checks-Effects-Interactions : vérifiez, modifiez l'état, puis faites les appels externes.

// ✓ Checks-Effects-Interactions function withdraw() public { uint amt = balances[msg.sender]; balances[msg.sender] = 0; // ↑ état modifié EN PREMIER payable(msg.sender).transfer(amt); }
💡 Mnémotechnique : Checks → Effects → Interactions. Dans cet ordre, toujours.
❌ Cause

Avant Solidity 0.8, les entiers débordaient silencieusement (255+1=0 sur uint8). En 0.8+ c'est corrigé par défaut, mais un unchecked mal utilisé le réintroduit.

// ❌ Dangereux en < 0.8 uint8 x = 255; x += 1; // → 0 silencieusement ! // ❌ unchecked risqué unchecked { x -= 10; // underflow possible }
✓ Solution

En Solidity 0.8+, l'overflow lève automatiquement une exception. N'utilisez unchecked que si vous êtes certain qu'il n'y a aucun risque.

// ✓ 0.8+ : safe by default uint256 x = 255; x += 1; // revert si overflow // unchecked seulement si garanti // ex: for (uint i=0; i<max; i++)
💡 Vérifiez votre pragma : si vous êtes en 0.8.x, vous êtes protégé par défaut. Ne descendez pas en 0.6 sans raison.
❌ Cause

Solidity 0.5+ exige que chaque fonction et variable d'état déclare explicitement sa visibilité. L'oubli produit une erreur de compilation.

// ❌ Visibilité manquante uint256 count; // pas de public/private function increment() { // pas de public count++; }
✓ Solution

Déclarez explicitement public, private, internal ou external sur chaque élément.

// ✓ Visibilités explicites uint256 public count; function increment() public { count++; } function _reset() private { count = 0; }
💡 Résumé : public = tout le monde · private = ce contrat · internal = contrat + héritiers · external = appels extérieurs uniquement.
❌ Cause

tx.origin retourne l'initiateur de la transaction, pas l'appelant direct. Un contrat intermédiaire malveillant peut usurper l'identité.

// ❌ tx.origin : dangereux function adminAction() public { require(tx.origin == owner); // contrat malveillant // peut tromper ce check ! }
✓ Solution

Utilisez toujours msg.sender pour vérifier l'identité de l'appelant direct. tx.origin ne doit jamais servir de contrôle d'accès.

// ✓ msg.sender : correct function adminAction() public { require(msg.sender == owner, "Not owner"); }
💡 Règle : msg.sender = qui appelle maintenant. tx.origin = qui a initié la chaîne. Pour l'auth, toujours msg.sender.