Le framework Anchor#
Développer un programme Solana en Rust pur est une experience formatrice, mais aussi fastidieuse. Le développeur doit manuellement désérialiser les données d’instruction, valider chaque compte (propriétaire, signataire, mutabilité), gérer l’allocation mémoire et écrire des centaines de lignes de code répétitif avant même d’implémenter la moindre logique métier. Anchor est né de ce constat : il fournit un cadre déclaratif qui automatise la plomberie et laisse le développeur se concentrer sur ce qui compte.
Anchor est un framework Rust pour Solana reposant sur un système de macros procédurales. Ces macros génèrent, à la compilation, tout le code de validation, de sérialisation et de routage des instructions. Le résultat est un programme plus court, plus lisible et surtout plus sûr : les erreurs de validation manuelles — oubli de vérifier un signataire, confusion entre comptes — sont eliminées par construction.
Ce chapitre constitue le coeur technique de la partie développement.
Nous y construirons un programme compteur complet, puis nous dissèquerons chaque composant du framework : la macro #[program], le trait Accounts, les contraintes de validation, l’objet Context, et la serialisation Borsh.
Programme complet : le compteur#
Avant d’analyser chaque composant séparément, voici le programme complet.
Il implémente un compteur on-chain avec deux instructions : initialize (création du compte) et incrément (incrémentation de la valeur).
use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = ctx.accounts.authority.key();
counter.count = 0;
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + 32 + 8,
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
has_one = authority,
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
#[account]
pub struct Counter {
pub authority: Pubkey,
pub count: u64,
}
Chaque élément de ce programme sera expliqué en détail dans les sections qui suivent. Gardez ce listing sous les yeux : toutes les notions abstraites que nous introduirons trouveront leur illustration concrète ici.
La macro #[program]#
La macro #[program] transforme un module Rust ordinaire en un programme Solana complet, avec routage automatique des instructions.
Définition 75 (Module programme)
La macro #[program] marque un module Rust comme le module principal d’un programme Solana.
Chaque fonction publique définie dans ce module devient un point d’entrée d’instruction (instruction endpoint).
Anchor génère automatiquement le code de routage : lorsqu’une transaction arrive, le discriminateur de l’instruction (les 8 premiers octets) détermine quelle fonction appeler.
#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// logique d'initialisation
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
// logique d'incrementation
Ok(())
}
}
Le discriminateur d’instruction est les 8 premiers octets de sha256("global:<nom_de_la_fonction>").
Le client doit inclure ce discriminateur au début des données d’instruction pour que le routage fonctionne.
Remarque 48 (Signatures des fonctions)
Chaque fonction d’instruction suit une convention stricte :
Le premier paramètre est toujours
ctx: Context<T>, ouTest une structure implémentant le traitAccounts. Ce paramètre donne accès aux comptes valides.Les parametres suivants (optionnels) représentent les données d’instruction, désérialisées automatiquement depuis les octets de la transaction. Par exemple :
pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()>.Le type de retour est toujours
Result<()>. En cas d’erreur, on retourne uneAnchorErrorqui sera propagée au client.
Cette uniformité permet à Anchor de générer un IDL (Interface Definition Language) complet, que les clients utilisent pour construire les transactions.
Remarque 49 (La macro declare_id!)
La macro declare_id!("...") déclare l’adresse on-chain du programme.
Cette adresse est la clé publique de la paire de clés utilisée lors du déploiement.
Anchor vérifie à l’exécution que le programme s’exécute bien à cette adresse, empêchant un attaquant de déployer un programme malveillant à une adresse différente et de le faire passer pour le programme légitime.
Le trait Accounts et #[derive(Accounts)]#
Plutôt que de vérifier manuellement chaque compte dans le corps de la fonction, on déclare les attentes de manière structurelle.
Définition 76 (Trait Accounts)
Le trait Accounts est le mécanisme central d’Anchor pour spécifier de manieèe déclarative quels comptes une instruction attend et comment ils doivent être validés.
En dérivant ce trait avec #[derive(Accounts)], le développeur déclare une structure dont chaque champ représente un compte.
Anchor génère alors automatiquement le code de désérialisation et de validation : vérification du propriétaire, du signataire, de la mutabilité, et de toute contrainte supplémentaire.
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + 32 + 8,
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
Les trois champs de Initialize illustrent les types de comptes Anchor :
counter: Account<'info, Counter>— le compte a créer. Le typeAccount<'info, Counter>indique qu’il doit être désérialisé comme unCounteret possédé par le programme courant.authority: Signer<'info>— le signataire qui paie la création. Le typeSignergarantit que ce compte a signé la transaction.system_program: Program<'info, System>— le programme système, requis pour créer des comptes. Le typeProgramvérifie l’adresse.
Définition 77 (Structure Increment)
La structure Increment est plus simple car elle ne crée aucun compte. Elle accède à un compte existant et vérifie que l’appelant en est l’autorité.
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
has_one = authority,
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
Ici, has_one = authority signifie : le champ counter.authority doit être égal à la clé publique du compte authority passé dans la transaction. Si ce n’est pas le cas, l’instruction échoue avant que la logique métier ne s’exécute.
Remarque 50 (Validation automatique)
Anchor génère toutes les vérifications avant l’exécution de la logique métier : propriétaire du compte (Account<'info, T> doit appartenir au programme), signataire (Signer doit avoir signé), mutabilité (#[account(mut)] doit etre writable), et discriminateur (les 8 premiers octets doivent correspondre au type).
Si l’une échoue, l’instruction est rejetée avec une erreur explicite. Le développeur n’écrit jamais ces vérifications manuellement.
Contraintes Anchor#
Les contraintes sont des annotations dans l’attribut #[account(...)] qui génèrent du code de validation à la compilation.
Définition 78 (Contrainte init)
La contrainte init crée et initialise un nouveau compte.
Anchor génère un appel CPI au programme système pour allouer l’espace, transférer les lamports de loyer et assigner le propriétaire au programme courant.
Le discriminateur est écrit dans les 8 premiers octets.
#[account(
init,
payer = authority,
space = 8 + 32 + 8,
)]
pub counter: Account<'info, Counter>,
La contrainte init exige deux paramètres supplémentaires :
payer: le compte qui paiera le loyer (rent) pour la création.space: la taille en octets du compte à allouer (discriminateur inclus).
Remarque 51 (Erreurs fréquentes avec init)
Trois erreurs fréquentes avec init :
Oublier le discriminateur : l’espace doit inclure 8 octets de discriminateur. Pour
Counter:8 + 32 + 8 = 48.Oublier le
system_program:initappelle le programme système via CPI, il faut doncpub system_program: Program<'info, System>.Oublier
#[account(mut)]sur le payeur : ses lamports diminuent, il doit être mutable.
Définition 79 (Contrainte mut)
La contrainte mut marque un compte comme mutable (writable).
Anchor vérifie que le compte est bien marqué writable dans la transaction.
Sans cette contrainte, toute tentative de modification des données du compte échouera.
#[account(mut)]
pub counter: Account<'info, Counter>,
Définition 80 (Contrainte has_one)
La contrainte has_one = field vérifie que la valeur du champ field dans le compte correspond à la clé publique d’un autre compte présent dans la structure.
C’est le mécanisme principal pour les vérifications d’autorisation.
#[account(
mut,
has_one = authority,
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
Anchor génère l’équivalent de : require!(counter.authority == authority.key(), ErrorCode::ConstraintHasOne).
Le nom du champ dans la structure de données (counter.authority) doit correspondre exactement au nom du champ dans la structure Accounts (authority).
Définition 81 (Contraintes seeds et bump)
Les contraintes seeds et bump dérivent et vérifient une Program Derived Address (PDA).
Anchor recalcule l’adresse à partir des seeds et vérifie qu’elle correspond à la clé publique du compte.
#[account(
init,
seeds = [b"counter", authority.key().as_ref()],
bump,
payer = authority,
space = 8 + 32 + 8,
)]
pub counter: Account<'info, Counter>,
Sans valeur explicite, Anchor calcule le canonical bump et le stocke dans ctx.bumps.
Lors des accès ulterieurs, on écrit bump = counter.bump pour éviter de le recalculer.
Définition 82 (Contrainte close)
La contrainte close = target ferme un compte en transférant tous ses lamports vers target et en remettant ses données à zéro.
#[account(
mut,
close = authority,
has_one = authority,
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
Apres l’exécution, le compte counter aura 0 lamport, ses données seront effacées, et le runtime Solana le supprimera à la fin de la transaction.
Définition 83 (Contrainte constraint)
La contrainte constraint = expr définit une condition booléenne arbitraire.
Si elle est évaluée à false, l’instruction échoue.
#[account(
mut,
constraint = counter.count < 1000 @ ErrorCode::CounterOverflow,
)]
pub counter: Account<'info, Counter>,
Le suffixe @ ErrorCode::CounterOverflow spécifie un code d’erreur personnalisé.
Sans ce suffixe, Anchor utilise une erreur générique ConstraintRaw.
Définition 84 (Contrainte address)
La contrainte address = expr vérifie que la clé publique du compte correspond exactement à une adresse connue.
#[account(
address = token::ID,
)]
pub token_program: AccountInfo<'info>,
Anchor génère : require!(token_program.key() == token::ID, ErrorCode::ConstraintAddress).
L’objet Context<T>#
Le Context encapsule tout ce dont une fonction d’instruction a besoin pour intéragir avec les comptes et l’environnement d’exécution.
Définition 85 (Context)
L’objet Context<T> est passé en premier paramètre de chaque fonction d’instruction.
Il fournit accès à :
ctx.accounts: la structureT(implémentantAccounts) contenant les comptes validés et désérialisés.ctx.program_id: la clé publique du programme en cours d’exécution.ctx.remaining_accounts: une tranche de comptes supplémentaires non déclarés dans la structure, utile pour les listes de longueur variable.ctx.bumps: unBTreeMap<String, u8>contenant les bumps calculés pour chaque PDA déclarée avecseedsetbump.
La structure Context est générique sur T : le type T détermine quels comptes sont disponibles et comment ils sont validés.
Exemple 23 (Accès aux comptes dans le corps d’une fonction)
Dans le corps d’une instruction, on accède aux comptes via ctx.accounts.
Chaque champ est directement accessible et déjà désérialisé :
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
msg!("Compteur incremente a : {}", counter.count);
Ok(())
}
La macro msg! écrit dans les logs de la transaction, visibles via les explorateurs de blocs ou les outils de développement.
A la fin de l’instruction, Anchor sérialise automatiquement les modifications apportées à counter dans le compte on-chain.
Remarque 52 (Remaining accounts)
Le champ ctx.remaining_accounts est un &[AccountInfo<'info>] contenant les comptes passés dans la transaction mais non déclarés dans la structure Accounts.
Ce mécanisme est utile lorsque le nombre de comptes varie d’un appel a l’autre.
Ces comptes ne bénéficient d’aucune validation automatique : le développeur doit effectuer toutes les vérifications manuellement.
Sérialisation Borsh#
Solana utilise Borsh pour la sérialisation de toutes les données on-chain.
Définition 86 (Borsh)
Borsh (Binary Object Representation Serializer for Hashing) est un format de sérialisation binaire déterministe : une même valeur produit toujours exactement la même séquence d’octets. Cette propriété est cruciale pour la blockchain, ou les données doivent être reproductibles et vérifiables par tous les validateurs.
Borsh encode les types primitifs en little-endian avec des tailles fixes : un u64 occupe toujours exactement 8 octets, un bool exactement 1 octet.
Les types de taille variable (comme String ou Vec<T>) sont prefixés par leur longueur sur 4 octets.
Définition 87 (Discriminateur de compte)
Le discriminateur de compte est un identifiant de 8 octets placé au début de chaque compte Anchor.
Il est calculé comme les 8 premiers octets de sha256("account:<NomDuCompte>").
Pour la structure Counter, le discriminateur est sha256("account:Counter")[..8].
Lors de la désérialisation, Anchor vérifie que les 8 premiers octets du compte correspondent au discriminateur attendu.
Si ce n’est pas le cas, la désérialisation échoue immédiatement, empêchant un compte d’un type d’être utilisé là ou un autre type est attendu.
La macro #[account] dérive automatiquement les traits nécessaires pour la sérialisation Borsh et le calcul du discriminateur.
#[account]
pub struct Counter {
pub authority: Pubkey, // 32 octets
pub count: u64, // 8 octets
}
La visualisation ci-dessous montre la disposition mémoire d’un compte Counter tel qu’il est stocké on-chain.
Remarque 53 (Calcul de l’espace)
L’espace total d’un compte Anchor est : 8 octets (discriminateur) + somme des tailles de chaque champ. Voici les tailles des types les plus courants :
Type |
Taille (octets) |
|---|---|
|
1 |
|
1 |
|
2 |
|
4 |
|
8 |
|
16 |
|
32 |
|
4 + longueur en octets |
|
4 + longueur * taille_de(T) |
|
1 + taille_de(T) |
Pour le compteur : 8 + 32 + 8 = 48 octets.
Il faut calculer l’espace maximal que le compte pourra occuper, car la taille est fixée à la creation (sauf avec realloc).
Flux complet d’une instruction#
Le cycle de vie complet d’une instruction montre comment tous les composants Anchor s’articulent.
Exemple 24 (Cycle de vie d’une instruction increment)
Pour l’instruction increment, le cycle est le suivant :
Réception : le runtime identifie le programme cible via
program_id.Routage : Anchor lit le discriminateur (8 premiers octets) et appelle
increment.Désérialisation des comptes : les comptes sont mappés sur
Increment, avec vérification du discriminateur, propriétaire, mutabilité, signataire ethas_one.Désérialisation des données : les paramètres supplémentaires sont désérialisés (aucun pour
increment).Exécution :
counter.count += 1s’exécute dans leContext<Increment>.Sérialisation : les comptes modifiés sont réécrits on-chain.
Retour : le résultat est renvoyé au client.
La visualisation suivante résume ce flux sous forme de diagramme.
Remarque 54 (Atomicité des transactions)
Si une erreur survient à n’importe quelle étape — échec de validation, erreur dans la logique métier, dépassement du budget de compute units — toutes les modifications sont annulées. La transaction est atomique : elle réussit entièrement ou échoue entièrement. Aucune modification partielle n’est jamais écrite on-chain.
Remarque 55 (Budget de compute units)
Chaque transaction Solana dispose d’un budget de compute units (CU) limitant le calcul autorisé : 200 000 CU par instruction, 1 400 000 CU par transaction. Un dépassement fait échouer la transaction. Les operations coûteuses incluent les appels CPI, les opérations cryptographiques et les allocations mémoire.
Résumé#
Ce chapitre a presenté le framework Anchor et ses composants fondamentaux à travers la construction d’un programme compteur complet.
Composant |
Rôle |
|---|---|
|
Marque un module comme programme Solana ; chaque fonction publique devient une instruction |
|
Dérive le trait |
|
Dérive la sérialisation Borsh et le discriminateur pour une structure de données |
|
Objet donnant accès aux comptes, au |
|
Contrainte pour créer et initialiser un nouveau compte |
|
Contrainte marquant un compte comme mutable |
|
Contrainte vérifiant la correspondance d’un champ avec un compte |
|
Contraintes pour dériver et vérifier une PDA |
|
Contrainte pour fermer un compte et récupérer le loyer |
|
Contrainte booléenne arbitraire |
|
Contrainte vérifiant l’adresse exacte d’un compte |
Borsh |
Format de sérialisation binaire déterministe utilisé par Solana |
Discriminateur |
8 premiers octets identifiant le type d’un compte ou d’une instruction |
Le programme compteur présente dans ce chapitre servira de base pour les chapitres suivants, ou nous explorerons la gestion avancée des comptes et des données, les tests, et les cross-program invocations.