Ce tableau compare les différents modèles d’exécution et de stockage entre Ethereum et Solana.
Concept
Ethereum
Solana
Impact pratique
Exécution
EVM (bytecode stack-based)
BPF (Berkeley Packet Filter, compile depuis Rust/C++)
Solana est plus rapide (JIT, native compilation)
State
Stocke dans le contrat (storage)
Comptes separes du programme
Solana : plus de parallelisme, pas de “global state”
Langage
Solidity (type, haut niveau)
Rust (système, verifie)
Rust est plus strict (ownership) mais plus performant
Adressage
Nonces séquentielles (contract creation count)
Hash des seeds du PDA
Solana : addresses déterministes, previsibles
Fees
Gas (variable selon complexite)
Compute units + rent exemption
Solana : couts predictibles, rent pour stockage durable
TPS
~15-30 (Layer 1)
~65,000 (théorique)
Solana : haut debit, sharding natif
Points cles : 1. Programs stateless : Le code est separe des données (comptes). Le programme ne contient pas d’etat. 2. Accounts model : Chaque compte est une entite separee avec son propre solde (Lamports) et ses données. 3. Rent exemption : Les comptes doivent avoir un solde minimum pour exister (rent). Au-dessus de ce seuil, pas de frais de stockage. 4. Parallelisme : Solana peut executer plusieurs transactions en parallele si elles touchent des comptes différents.
Implications pour les developpeurs : - Sur Ethereum : on deploye un contrat avec tout l’etat (mappings, arrays) - Sur Solana : on deploye un programme (code) et on créé des comptes PDAs pour chaque utilisateur/entite - Le modèle Solana permet plus de parallelisme mais demande plus de planification (structure des comptes)
Note technique : BPF (Berkeley Packet Filter) est un format bytecode securise qui s’execute dans le noyau Linux. Solana l’utilise pour la securite et la performance (JIT compilation vers machine code).
3. PDAs (Program Derived Addresses)
Les PDAs sont des adresses derivees déterministes.
# PDA explanationprint("""PDA (Program Derived Address)PROPRIETES:- Adresse derivee du program_id et de seeds- Pas de cle privee (programme seulement)- Deterministe: meme seeds = meme PDA- Bump: [b"seed", bump] pour trouver le PDAGENERATION:seeds = [b"counter", authority.key().as_ref()](pda, bump) = Pubkey::find_program_address(&seeds, program_id)UTILISATION:1. Stockage de donnees utilisateur2. Escrows3. Token accounts4. MetadataEXEMPLE DANS ANCHOR:#[account( seeds = [b"counter", authority.key().as_ref()], bump)]pub counter: Account<'info, Counter>,""")
PDA (Program Derived Address)
PROPRIETES:
- Adresse derivee du program_id et de seeds
- Pas de cle privee (programme seulement)
- Deterministe: meme seeds = meme PDA
- Bump: [b"seed", bump] pour trouver le PDA
GENERATION:
seeds = [b"counter", authority.key().as_ref()]
(pda, bump) = Pubkey::find_program_address(&seeds, program_id)
UTILISATION:
1. Stockage de donnees utilisateur
2. Escrows
3. Token accounts
4. Metadata
EXEMPLE DANS ANCHOR:
#[account(
seeds = [b"counter", authority.key().as_ref()],
bump
)]
pub counter: Account<'info, Counter>,
Interpretation : Programme Counter Anchor
Ce code presente un programme de compteur simple avec les concepts fondamentaux d’Anchor.
Composant
Rôle
Details
#[program]
Module de fonctions publiques
Points d’entree du programme (appelables par les clients)
Context<Initialize>
Validation des comptes
Anchor verifie que tous les comptes passes sont valides
#[account(init)]
Creation de compte
Alloue un nouveau compte PDA avec init + payer + space + seeds
#[account(mut)]
Compte modifiable
Le compte peut etre modifie pendant la transaction
#[error_code]
Erreurs custom
Erreurs spécifiques au programme (ici: Underflow)
Points cles : 1. initialize créé un PDA counter avec seeds [b"counter", authority.key().as_ref()] → 1 counter par utilisateur 2. space = 8 + Counter::INIT_SPACE : 8 octets de discriminant Anchor + taille de la struct Counter (32 + 8 = 40) 3. increment et decrement utilisent les mêmes seeds pour retrouver le PDA du counter 4. require!(counter.count > 0, CounterError::Underflow) : protection contre underflow (custom error)
Comparaison Solidity vs Anchor : - Solidity : mapping(address => uint256) public counters; (stocke dans le contrat) - Anchor : Chaque utilisateur a son propre compte PDA Counter (separe du programme)
Avantages du modèle Solana : - Parallelelisme : chaque utilisateur modifie son propre compte (pas de contention) - Rent exemption : l’utilisateur paie pour son propre compte (pas le deployeur) - Flexibilite : les comptes peuvent etre fermes pour recuperer la rent
Note technique : Le discriminant Anchor (8 octets) est un hash unique qui identifie le type de compte. Il permet a Anchor de verifier qu’un compte passe est bien du type attendu.
Exercice : Générateur de struct Anchor
Ecrire un programme Anchor pour gerer un profil utilisateur sur Solana. Completez la fonction generate_anchor_profile qui produit le code Rust du programme.
Objectifs : 1. Définir une struct UserProfile avec les champs : owner (Pubkey), username (String[32]), bio (String[280]), reputation (u64) 2. Implementer les fonctions create_profile et update_reputation 3. Calculer l’espace necessaire pour le compte
Indices : - Utiliser #[account(init, payer = owner, space = 8 + UserProfile::INIT_SPACE, seeds = [...], bump)] pour la creation - Le PDA est derive de b"profile" et de l’adresse du proprietaire - update_reputation prend un montant positif ou negatif (utiliser i64 pour l’argument)
def generate_anchor_profile() ->str:"""Generer le code Rust d'un programme Anchor UserProfile. Le programme doit definir : - Struct UserProfile : owner (Pubkey), username (String[32]), bio (String[280]), reputation (u64) - Fonction create_profile : initialise le profil avec username et bio - Fonction update_reputation : ajoute un delta (positif ou negatif) a la reputation Retourner le code Rust sous forme de chaine.TODO etudiant : implementez generate_anchor_profile. Indice : inspirez-vous du programme Counter vu en section 2. Les seeds du PDA seront [b"profile", owner.key().as_ref()]. """# TODO etudiant : ecrivez le code Rust completreturn""# TODO etudiant : remplacez par le code du programme# Test : afficher le code generecode = generate_anchor_profile()if code:print("Programme UserProfile Anchor :")print(code)else:print("Exercice a completer : generate_anchor_profile")
Exercice a completer : generate_anchor_profile
4. CPI (Cross-Program Invocation)
# CPI exampleCPI_EXAMPLE ='''// CPI vers System Program pour transfer SOLuse anchor_lang::system_program;pub fn transfer_sol(ctx: Context<TransferSol>, amount: u64) -> Result<()> { let cpi_context = CpiContext::new( ctx.accounts.system_program.to_account_info(), system_program::Transfer { from: ctx.accounts.from.to_account_info(), to: ctx.accounts.to.to_account_info(), } ); system_program::transfer(cpi_context, amount)}#[derive(Accounts)]pub struct TransferSol<'info> { #[account(mut)] pub from: Signer<'info>, #[account(mut)] pub to: AccountInfo<'info>, pub system_program: Program<'info, System>,}// CPI vers Token Programuse anchor_spl::token::{self, Transfer as TokenTransfer};pub fn transfer_token( ctx: Context<TransferToken>, amount: u64) -> Result<()> { let cpi_accounts = TokenTransfer { from: ctx.accounts.from.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); token::transfer(CpiContext::new(cpi_program, cpi_accounts), amount)}'''print("CPI Examples:")print(CPI_EXAMPLE)
CPI Examples:
// CPI vers System Program pour transfer SOL
use anchor_lang::system_program;
pub fn transfer_sol(ctx: Context<TransferSol>, amount: u64) -> Result<()> {
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
}
);
system_program::transfer(cpi_context, amount)
}
#[derive(Accounts)]
pub struct TransferSol<'info> {
#[account(mut)]
pub from: Signer<'info>,
#[account(mut)]
pub to: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
// CPI vers Token Program
use anchor_spl::token::{self, Transfer as TokenTransfer};
pub fn transfer_token(
ctx: Context<TransferToken>,
amount: u64
) -> Result<()> {
let cpi_accounts = TokenTransfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
token::transfer(CpiContext::new(cpi_program, cpi_accounts), amount)
}
Interpretation : PDAs (Program Derived Addresses)
Les PDAs sont une innovation unique de Solana : des adresses sans cle privee, controlees par des programmes.
Propriete
Description
Impact
Pas de cle privee
Le PDA est derive de program_id + seeds
Seul le programme peut signer pour le PDA
Déterministe
Mêmes seeds = même PDA (toujours)
Pas de collision, pas de generation aleatoire
Bump canonique
Trouve le premier bump (1-255) qui donne une adresse hors courbe ed25519
Garantit que seul le programme peut signer
Points cles : 1. seeds = [b"counter", authority.key().as_ref()] : le PDA est unique par utilisateur 2. Le bump est la valeur qui fait que l’adresse est “hors courbe” (pas de cle privee correspondante) 3. Anchor calcule automatiquement le bump avec #[account(seeds = [...], bump)] 4. Les PDAs sont utilises pour : comptes de données utilisateur, vaults, escrows, metadata NFT
Cas d’usage typiques : - Compte utilisateur : seeds = [b"user", user_pubkey.as_ref()] → 1 compte par utilisateur - Vault d’escrow : seeds = [b"vault", escrow_id.as_ref()] → vault unique par escrow - Metadata NFT : seeds = [b"metadata", mint.as_ref()] → metadata unique par mint
Generation manuelle (Rust) :
let (pda, bump) =Pubkey::find_program_address(&[b"counter", authority.key().as_ref()], program_id);
Note technique : Le “bump” est entre 1 et 255. Si 255 ne suffit pas, il n’y a pas de PDA valide. Anchor utilise le “bump canonique” (le premier qui marche) pour garantir l’unicite.
Exercice : Calculateur de PDA et espace de compte
Sur Solana, chaque compte doit allouer un espace de stockage payant (rent). La taille depend des champs de la struct Anchor. Completez les fonctions calculate_pda et calculate_account_size.
Objectifs : 1. Deriver une PDA a partir de seeds et d’un program_id (simulation) 2. Calculer l’espace necessaire pour un compte Anchor (discriminant 8 octets + taille des champs) 3. Estimer le cout en SOL (rent) pour créer le compte
Indices : - L’espace Anchor = 8 octets (discriminant) + somme des tailles des champs - Types Anchor : Pubkey = 32 octets, u64 = 8 octets, bool = 1 octet, String = 4 + longueur, Vec = 4 + N * taille(T) - Le rent sur Solana est environ 0.000890880 SOL par octet (a la creation)
import hashlib# Tailles des types Anchor (en octets)ANCHOR_TYPE_SIZES = {"Pubkey": 32, "u64": 8, "u32": 4, "u16": 2, "u8": 1,"i64": 8, "i32": 4, "bool": 1, "f64": 8,}RENT_PER_BYTE =0.000890880# SOL par octet (approximation Solana)def calculate_pda(seeds: list, program_id: str) ->str:"""Simuler la derivation d'une PDA Solana. La vraie PDA utilise ed25519 off-curve. Ici on simule avec SHA-256.TODO etudiant : Etape 1 : concatener les seeds et le program_id Etape 2 : hasher avec SHA-256 Etape 3 : retourner le hash hexadecimal comme "PDA simulee" Args: seeds: liste de bytes ou strings (les seeds du PDA) program_id: adresse du programme (hex string) Returns: str : la PDA simulee (hash hex) """# TODO etudiant : implementez calculate_pdareturn""# TODO etudiantdef calculate_account_size(fields: list) ->int:"""Calculer l'espace necessaire pour un compte Anchor. Args: fields: liste de tuples (nom_du_champ, type_du_champ, taille_optionnelle) ex: [("authority", "Pubkey", None), ("count", "u64", None), ("name", "String", 32)] Returns: int : taille totale en octets (incluant le discriminant Anchor de 8 octets)TODO etudiant : Etape 1 : ajouter 8 octets pour le discriminant Anchor Etape 2 : pour chaque champ, ajouter la taille du type (utiliser ANCHOR_TYPE_SIZES) Etape 3 : pour String, ajouter 4 + taille_max ; pour Vec, ajouter 4 + N * taille_element """# TODO etudiant : implementez calculate_account_sizereturn0# TODO etudiant# Test : compte Counter avec authority + countfields = [ ("authority", "Pubkey", None), ("count", "u64", None),]size = calculate_account_size(fields)pda = calculate_pda([b"counter", b"alice_pubkey"], "Counter11111...11111")rent = size * RENT_PER_BYTE if size >0else0print(f"Espace compte : {size} octets")print(f"PDA simulee : {pda[:32]}...")print(f"Rent estime : {rent:.6f} SOL")print("Exercice a completer : calculate_pda et calculate_account_size")
Espace compte : 0 octets
PDA simulee : ...
Rent estime : 0.000000 SOL
Exercice a completer : calculate_pda et calculate_account_size
5. Commandes Anchor CLI
# Anchor CLI commandsprint("""INSTALLATION:cargo install --git https://github.com/coral-xyz/anchor avm --locked --forceavm install latestavm use latestCREATION PROJET:anchor init my_programcd my_programSTRUCTURE:my_program/|-- Anchor.toml # Configuration|-- Cargo.toml|-- programs/| `-- my_program/| |-- Cargo.toml| `-- src/| `-- lib.rs # Code principal|-- tests/| `-- my_program.ts # Tests`-- app/ # FrontendCOMMANDES:anchor build # Compileranchor test # Executer les testsanchor deploy # Deploieranchor run <script> # Executer un scriptCLUSTER:anchor test --skip-local-validator # Avec validator externeanchor deploy --provider.cluster devnet""")
Les CPIs permettent a un programme Solana d’appeler un autre programme (equivalent des delegatecall en Solidity).
Type de CPI
Programme cible
Opération
system_program::transfer
System Program
Transferts de SOL native
token::transfer
SPL Token Program
Transferts de tokens SPL
Metaplex CPI
Metaplex
Creation NFT, metadata
Custom CPI
Autres programmes utilisateur
Composition de fonctionnalites
Points cles : 1. CpiContext::new créé un contexte CPI standard (signataire = l’appelant) 2. CpiContext::new_with_signer permet a un PDA de signer (cas du vault escrow) 3. Tous les comptes necessaires doivent etre passes dans la struct Accounts (Anchor verifie la coherence) 4. Les CPIs sont payables par l’appelant (compute units + rent exemption)
Différence EVM vs Solana : - EVM : delegatecall execute le code d’un autre contrat DANS le contexte du contrat appelant - Solana : CPI execute le code d’un autre programme AVEC ses propres comptes (contexte separe)
Pattern CPI classique : 1. Définir les comptes necessaires dans la struct #[derive(Accounts)] 2. Créer le CpiContext avec les bons comptes et autorites 3. Appeler la fonction du programme cible (ex: token::transfer)
Note technique : Les CPIs sont COMPOSES sur Solana. Un programme A peut appeler B, qui appelle C, etc. La limite de profondeur est de 4 appels chaînes.
Exercice : Analyseur de structure de comptes Anchor
L’avantage principal de Solana sur Ethereum est le parallelisme des transactions : deux transactions qui modifient des comptes différents peuvent s’executer en parallele. Completez la fonction analyze_parallelism qui determine quelles transactions peuvent s’executer simultanement.
Objectifs : 1. Identifier les comptes en lecture et ecriture pour chaque instruction 2. Determiner quelles paires de transactions sont parallelisables 3. Calculer le throughput théorique (nombre de transactions executables en parallele)
Indices : - Deux transactions sont parallelisables si elles n’ont aucun compte en ecriture en commun - Un compte en lecture seule ne bloque pas le parallelisme - Le throughput théorique = nombre maximum de transactions parallelisables dans un même batch
def analyze_parallelism(transactions: list) ->dict:"""Analyser le parallelisme possible entre transactions Solana. Args: transactions: liste de dict, chaque dict contient : - "name": nom de la transaction - "reads": set de comptes lus - "writes": set de comptes ecrits Returns: dict avec : - "parallelizable_pairs": liste de tuples (i, j) parallelisables - "conflicts": liste de tuples (i, j) en conflit - "max_parallel": nombre max de tx parallelisables - "sequential_only": True si aucune paire n'est parallelisableTODO etudiant : implementez analyze_parallelism. Etape 1 : pour chaque paire (i, j), verifier si writes[i] et writes[j] s'intersectent Etape 2 : si pas d'intersection des writes, la paire est parallelisable Etape 3 : calculer max_parallel (taille du plus grand sous-ensemble mutuellement parallelisable) """# TODO etudiant : implementez analyze_parallelismprint("Exercice a completer : analyze_parallelism")return {"parallelizable_pairs": [], "conflicts": [], "max_parallel": 0, "sequential_only": None}# Exemple : transactions Solana typiquestxs = [ {"name": "increment_counter_Alice", "reads": {"counter_alice"}, "writes": {"counter_alice"}}, {"name": "increment_counter_Bob", "reads": {"counter_bob"}, "writes": {"counter_bob"}}, {"name": "transfer_SOL_Alice_to_Bob", "reads": {"alice_wallet", "bob_wallet"}, "writes": {"alice_wallet", "bob_wallet"}}, {"name": "mint_token", "reads": {"mint_authority"}, "writes": {"mint_authority", "token_account"}}, {"name": "read_counter_Alice", "reads": {"counter_alice"}, "writes": set()},]result = analyze_parallelism(txs)print(f"Paires parallelisables : {len(result['parallelizable_pairs'])}")print(f"Conflits : {len(result['conflicts'])}")print(f"Max parallel : {result['max_parallel']}")print(f"Sequential only : {result['sequential_only']}")
Exercice a completer : analyze_parallelism
Paires parallelisables : 0
Conflits : 0
Max parallel : 0
Sequential only : None
6. Exemples guidés
# Exercice: Token EscrowEXERCISE_ESCROW ='''#[program]pub mod escrow { use super::*; pub fn make( ctx: Context<Make>, seed: u64, receive_amount: u64, ) -> Result<()> { let escrow = &mut ctx.accounts.escrow; escrow.seed = seed; escrow.initializer = ctx.accounts.initializer.key(); escrow.mint_a = ctx.accounts.mint_a.key(); escrow.mint_b = ctx.accounts.mint_b.key(); escrow.receive_amount = receive_amount; // Transfer tokens to vault token::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), token::Transfer { from: ctx.accounts.deposit.to_account_info(), to: ctx.accounts.vault.to_account_info(), authority: ctx.accounts.initializer.to_account_info(), }, ), ctx.accounts.deposit.amount, ) } pub fn take(ctx: Context<Take>) -> Result<()> { // Transfer B to initializer token::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), token::Transfer { from: ctx.accounts.depositor_token_b.to_account_info(), to: ctx.accounts.initializer_token_b.to_account_info(), authority: ctx.accounts.depositor.to_account_info(), }, ), ctx.accounts.escrow.receive_amount, )?; // Transfer A to depositor token::transfer( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), token::Transfer { from: ctx.accounts.vault.to_account_info(), to: ctx.accounts.depositor_token_a.to_account_info(), authority: ctx.accounts.escrow.to_account_info(), }, &[&[ b"escrow", ctx.accounts.maker.key().as_ref(), &ctx.accounts.escrow.seed.to_le_bytes()[..], &[ctx.bumps.escrow], ]], ), ctx.accounts.vault.amount, ) }}'''print("Exercice Escrow:")print(EXERCISE_ESCROW)
Exercice Escrow:
#[program]
pub mod escrow {
use super::*;
pub fn make(
ctx: Context<Make>,
seed: u64,
receive_amount: u64,
) -> Result<()> {
let escrow = &mut ctx.accounts.escrow;
escrow.seed = seed;
escrow.initializer = ctx.accounts.initializer.key();
escrow.mint_a = ctx.accounts.mint_a.key();
escrow.mint_b = ctx.accounts.mint_b.key();
escrow.receive_amount = receive_amount;
// Transfer tokens to vault
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.deposit.to_account_info(),
to: ctx.accounts.vault.to_account_info(),
authority: ctx.accounts.initializer.to_account_info(),
},
),
ctx.accounts.deposit.amount,
)
}
pub fn take(ctx: Context<Take>) -> Result<()> {
// Transfer B to initializer
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.depositor_token_b.to_account_info(),
to: ctx.accounts.initializer_token_b.to_account_info(),
authority: ctx.accounts.depositor.to_account_info(),
},
),
ctx.accounts.escrow.receive_amount,
)?;
// Transfer A to depositor
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.depositor_token_a.to_account_info(),
authority: ctx.accounts.escrow.to_account_info(),
},
&[&[
b"escrow",
ctx.accounts.maker.key().as_ref(),
&ctx.accounts.escrow.seed.to_le_bytes()[..],
&[ctx.bumps.escrow],
]],
),
ctx.accounts.vault.amount,
)
}
}
Interpretation : Commandes Anchor CLI
Ces commandes couvrent le cycle de vie complet de développement d’un programme Solana avec Anchor.
Commande
Action
Usage typique
anchor init my_program
Créer un nouveau projet
Demarrage d’un nouveau programme
anchor build
Compiler le programme Rust
Après modification du code
anchor test
Executer les tests TypeScript
Verification avant deploiement
anchor deploy
Deploier sur le cluster configure
Mise en production (devnet/mainnet)
anchor run <script>
Executer un script TypeScript
Opérations one-off (mint, init, etc.)
Points cles : 1. La structure de projet separe le code (programs/) des tests (tests/) et du frontend (app/) 2. Anchor.toml contient la configuration du cluster (localnet, devnet, mainnet) et les features du programme 3. Les tests sont ecrits en TypeScript (avec @coral-xyz/anchor) et interagissent avec le programme comme un client le ferait 4. --skip-local-validator permet de se connecter a un validator externe (devnet Solana publique) au lieu de lancer un validator local
Workflow typique :
anchor init mon_program # Créer le projet
anchor build # Compiler (verifier la syntaxe Rust)
anchor test # Executer les tests (verifier la logique)
anchor deploy # Deploier sur devnet
anchor run scripts/mint.ts # Executer des opérations d'initialisation
Note technique : Anchor utilise avm (Anchor Version Manager) pour gerer les versions multiples. avm install latest installe la dernière version, avm use latest l’active.
7. Contributions etudiantes – exemples guides
Les trois exemples ci-dessous, contribues par un groupe d’etudiants, illustrent les concepts fondamentaux de Solana et Anchor vus dans ce notebook : derivation de PDA, structure d’un programme Anchor, et modèle de comptes Solana. Chaque enonce est suivi de sa solution commentee.
Exemple guide 1 : Simulation Python de la derivation d’un PDA
Exemple contribue par le groupe Mehdi Azouz, Ryad Gazenay & Hani Bouzida (PR #2189).
Un PDA (Program Derived Address) est obtenu en cherchant un bump (255 -> 0) tel que le hash des seeds soit “hors courbe” ed25519 – ici simule par la condition premier octet < 128. La derivation doit etre déterministe (mêmes seeds -> même PDA) et unique par utilisateur (seeds différentes -> PDAs différents).
# Exercice 1 : Simulation Python de derivation PDAimport hashlibdef derive_pda_simulee(seeds: list, program_id: str) ->tuple:""" Simule la derivation d'un PDA Solana en Python. Convention simplifiee : on cherche le premier bump (255..0) tel que SHA256(b"".join(seeds) + program_id.encode() + bytes([bump])) produise un hash dont le premier octet (int) est < 128 (simule "hors courbe ed25519"). Args: seeds : liste de bytes (ex: [b"counter", b"alice_pubkey"]) program_id: identifiant du programme (str) Returns: (pda_address: str, bump: int) ou (None, None) si aucun bump trouve """ seeds_bytes =b"".join(seeds)for bump inrange(255, -1, -1): data = seeds_bytes + program_id.encode() +bytes([bump]) h = hashlib.sha256(data).hexdigest()ifint(h[:2], 16) <128:return h, bumpreturnNone, None# --- Tests ---seeds_alice = [b"counter", b"alice_pubkey_example"]program_id ="MyCounter1111111111111111111111111"pda, bump = derive_pda_simulee(seeds_alice, program_id)print(f"PDA alice : {pda[:16]}...")print(f"Bump : {bump}")pda2, _ = derive_pda_simulee(seeds_alice, program_id)print(f"Deterministe : {pda == pda2}")seeds_bob = [b"counter", b"bob_pubkey_example"]pda_bob, _ = derive_pda_simulee(seeds_bob, program_id)print(f"PDA bob != alice : {pda != pda_bob}")
PDA alice : 60ff9343476e7bb2...
Bump : 255
Deterministe : True
PDA bob != alice : True
Exemple guide 2 : Programme Anchor de vote complet
Exemple contribue par le groupe Mehdi Azouz, Ryad Gazenay & Hani Bouzida (PR #2189).
Un programme Anchor de sondage : create_poll initialise le compte Poll (un PDA derive de [b"poll", authority]), et vote incremente le compteur du choix avec garde sur la fermeture du sondage et la validite du choix. Le code Rust complet est presente ci-dessous.
Exemple guide 3 : Modeliser un Counter Solana en Python
Exemple contribue par le groupe Mehdi Azouz, Ryad Gazenay & Hani Bouzida (PR #2189).
Pour valider la comprehension du modèle de comptes Solana, on modelise en Python pur le programme Counter Anchor : initialize créé un compte count = 0, increment/decrement verifient l’authority avant toute modification, et decrement protege contre l’underflow (comme le ferait Anchor).
Exercice (a completer) : Etendre le Counter avec un plafond et un reset
A votre tour ! En vous inspirant de l’exemple guide 3, faites evoluer la classe CounterProgram :
Ajoutez une borne MAX_COUNT = 10 : increment doit lever une exception (interceptee) si l’on depasse le plafond.
Ajoutez une méthode reset(counter, authority) qui remet count a 0 – seule l’authority du compte peut la declencher.
Ecrivez un petit scénario de test qui incremente jusqu’au plafond, tente un increment de trop (try/except), puis effectue un reset.
Indice : reprenez le pattern de verification authority != counter.data["authority"] déjà utilise dans increment/decrement.
# Exercice : etendre CounterProgram avec MAX_COUNT (plafond) et reset(counter, authority)# TODO etudiant :# Etape 1 : sous-classer ou modifier CounterProgram avec MAX_COUNT = 10# Etape 2 : increment leve une exception (interceptee) si count atteindrait le plafond# Etape 3 : reset(counter, authority) remet count a 0, reserve a l'authority# Etape 4 : scenario de test (increment jusqu'au plafond, increment de trop, reset)print("Exercice a completer")
Points cles : 1. make créé un compte escrow avec seeds [b"escrow", maker.key(), seed] (PDA déterministe) 2. Le vault est un compte token PDA contrôle par l’escrow (pas de cle privee) 3. take utilise CpiContext::new_with_signer avec les seeds de l’escrow PDA pour autoriser le transfert depuis le vault 4. L’echange est atomique : soit les 2 transferts reussissent, soit aucun (revert si l’un echoue)
Securite : - Le seed u64 permet a un utilisateur d’avoir plusieurs escrows simultanes - Le bump ctx.bumps.escrow est inclus dans la signature CPI pour prouver l’autorite du PDA - L’ordre des transferts est important : d’abord le token B du depositor, puis le token A du vault (evite certains attaques)
Note technique : Sur Solana, les escrows sont des PDAs (pas de contrats intelligents avec etat). Le PDA signe les transferts via new_with_signer, ce qui est impossible sur EVM.
Resume et perspectives
Ce notebook a permis de decourir l’ecosysteme Solana et le framework Anchor comme alternative a l’ecosysteme Ethereum/Solidity. Nous avons compare les architectures : modèle de comptes stateless et exécution BPF haute performance (~65 000 TPS théoriques) chez Solana versus stockage dans le contrat et EVM chez Ethereum. Nous avons implemente un programme Counter en Rust avec Anchor, explorant les annotations #[program], #[account] et #[derive(Accounts)] qui abstraient la complexite du modèle de comptes. Nous avons etudie les PDAs (Program Derived Addresses) pour le stockage déterministe et securise de données utilisateur, les CPIs (Cross-Program Invocations) pour la composition de programmes, et implémente un escrow atomique de tokens SPL combinant CpiContext::new et CpiContext::new_with_signer pour les transfers signes par PDA.
La différence philosophique majeure entre Solana et Ethereum reside dans la separation stricte entre code (programmes stateless) et données (comptes independants). Ce modèle enable le parallelisme natif des transactions et elimine la contention globale, mais exige une planification plus rigoureuse de la structure des comptes. Les PDAs remplacent les mappings Solidity par des adresses déterministes sans cle privee, offrant une securite inherente contre les acces non autorises. Le framework Anchor simplifie considerablement le développement en Rust tout en preservant les garanties de securite du type system.
Le notebook suivant, SC-23-Cross-Chain-Python, aborde l’interoperabilite entre blockchains et les protocols de communication cross-chain comme Chainlink CCIP.