# Délégations

> Comment une fonction coûteuse obtient un circuit qui lui est propre sans faire grossir les circuits d’instructions. L’appel, l’ancre qui apparie chaque demande à exactement une invocation, les six circuits et leurs aspects économiques.

Le hachage et l’arithmétique des grands entiers dominent les charges de travail réelles : dans un bloc Ethereum du réseau principal, la multiplication et l’élévation au carré dans le corps de secp256k1 représentaient à elles seules 44 % des cycles avant d’être déléguées. Les prouver instruction par instruction est possible, et lent. Une **délégation** donne à une telle fonction une famille de circuits qui lui est propre, invoquée depuis le programme invité : les circuits d’instructions restent ainsi petits, et un programme ne paie que les délégations qu’il appelle.

## L’appel

Une délégation est invoquée, jamais décodée. Le programme invité écrit en RAM un cadre de mots de 32 bits et émet un `ecall` avec le numéro de la délégation dans `a7` et l’adresse de base du cadre dans `a0`. L’`ecall` est une ligne de la famille `ADD_SUB_LUI_AUIPC`, la **demande**. Le travail est une ligne de la famille propre à la délégation, l’**invocation**, qui lit chaque mot du cadre et réécrit chaque mot du cadre, résultats compris, au cycle de la demande. Une invocation ne possède aucun cycle; elle emprunte celui qui l’a demandée.

Les cadres sont alignés sur des mots et se trouvent entièrement en RAM ordinaire : aucun cadre ne chevauche donc une fenêtre publique ni les données auxiliaires (*advice*), et les lectures et écritures du cadre sont des requêtes mémoire ordinaires. Ce qu’a calculé une délégation est par conséquent lié exactement comme l’est tout rangement : par l’unique multiensemble mémoire.

## L’ancre

Demandes et invocations doivent s’apparier une à une : sinon, plusieurs demandes pourraient se refermer sur une seule invocation et laisser des appels non exécutés, ou une invocation non demandée pourrait réécrire un cadre. Elles s’apparient par le même multiensemble mémoire, dans un **espace d’ancrage** qui n’appartient qu’au type de délégation et qu’aucune instruction ne peut atteindre :

| | Lectures | Écritures |
| --- | --- | --- |
| Demande, cycle `c` | `T(s, base, 0, 0)` | `T(s, base, 4c + 3, v)` |
| Invocation | `T(s, base, 4c + 3, v′)` | `T(s, base, 0, 0)` |

Trois portes du côté de la demande fixent sa lecture à l’horodatage 0 et à la valeur 0, et lui font écrire 0 dans `a0`. Les tuples horodatés 0 sont alors exactement les lectures des demandes et les réponses des invocations : il y a donc autant d’invocations que de demandes, sur les mêmes bases; et puisque deux demandes ne partagent jamais un cycle, la lecture de chaque invocation est exactement l’écriture d’une seule demande. Chaque invocation se situe à la base et au cycle de sa demande. Aucune porte d’un circuit de délégation n’a eu à connaître quoi que ce soit des demandes.

## Plusieurs appels, une seule opération

Une opération trop large pour une seule ligne correspond à plusieurs invocations sur un même cadre, un mot du cadre désignant l’étape : une permutation keccak-f[1600] représente 24 appels de tour, une compression SHA-256 16 appels de quatre tours, une addition complète de points trois appels. Aucune porte ne relie deux lignes. Chaque appel prouve son étape sur le cadre tel qu’il le trouve, ses lectures se situant sur l’historique mémoire unique de chaque mot : il lit donc les écritures de l’étape précédente. C’est au code appelant de garantir que chaque étape s’exécute, dans l’ordre, et ce code appelant est du code de programme invité, prouvé sous forme d’instructions. Le SDK émet chaque opération en plusieurs appels à partir d’une seule fonction : un programme invité n’ordonne donc jamais les étapes à la main.

## Déclarées statiquement

Le balayage des instructions ne peut pas voir un appel, parce que le numéro est une valeur de `a7` connue à l’exécution. Chaque shim du SDK laisse donc un enregistrement de déclaration de 12 octets dans sa propre section d’édition de liens, conservée seulement si le shim est atteignable. La dérivation du programme recherche ces enregistrements dans l’image, et une famille déclarée rejoint la configuration, liée par l’identité à travers les octets de l’image. Une famille liée mais jamais appelée prouve zéro shard; un numéro appelé dont le programme n’a jamais déclaré la famille n’a pas de preuve.

## Les six circuits

| Famille | Une invocation | Construite à partir de |
| --- | --- | --- |
| `KECCAK_F` | un tour de keccak-f[1600] sur un cadre de 51 mots | des octets : 1 020 lookups `XOR8` par tour; les rotations comme formes linéaires sur des octets et des copies masquées |
| `SHA256_COMP` | quatre tours et quatre mots de l’expansion du message | des octets et `XOR8` : 52 obligations par tour, 32 par mot d’expansion; `Ch` et `Maj` comme formes linéaires en XOR |
| `POSEIDON2` | une permutation de largeur 3 | les tours calculés dans les couches propres du circuit, trois listes de portes par tour, sans aucun lookup; la seule délégation qui calcule au-dessus de sa première couche |
| `FR_ARITH` | une addition, une multiplication ou une inversion dans `Fr`, sous forme de Montgomery | des décompositions en bits et des chaînes de canonicité par rapport à `p` |
| `MOD_MUL` | un `a·b mod m` sur 256 bits, quatre modules d’Ethereum | des limbs de 32 bits, un quotient, des retenues, et une chaîne de canonicité qui prouve `out < m` |
| `EC_ADD` | un tiers d’une addition complète de points sur secp256k1 ou sur G1 de BN254 | la formule complète de Renes–Costello–Batina, sous forme de trois réductions par ligne |

Quelques constructions reviennent d’un circuit à l’autre. Une **règle du code unique** décode un mot du cadre qui désigne l’un de `k` cas en sélecteurs booléens dont exactement un est activé, parce que les codes s’additionnent : sans elle, les sélecteurs 1 et 3 répondent à une demande de 4. Une **chaîne de canonicité** prouve qu’une valeur de 256 bits est inférieure à un module au moyen d’emprunts sur des limbs de 32 bits. Enfin, chaque mot écrit est borné sous `2^32`, pour que la RAM reste faite de mots, ce sur quoi s’appuie chaque famille d’instructions.

## Aspects économiques

La hauteur d’une famille de délégation fixe le nombre d’appels que contient un shard, et un shard coûte sa hauteur quel que soit son taux d’occupation :

| Famille | Hauteur | Unités par shard | Preuve de shard |
| --- | --- | --- | --- |
| `KECCAK_F` | `2^18` | 10 922 permutations | 381 100 B |
| `SHA256_COMP` | `2^18` | 16 384 compressions | 189 988 B |
| `EC_ADD` | `2^16` | 21 845 additions | 434 916 B |
| `MOD_MUL` | `2^16` | 65 536 multiplications | 135 220 B |
| `POSEIDON2` | `2^8` | 256 permutations | 664 780 B |
| `FR_ARITH` | `2^8` | 256 opérations | 266 292 B |

Pour une famille souvent appelée, c’est le shard le plus gros qui revient le moins cher : une preuve `KECCAK_F` grossit à peine de `2^16` à `2^18`. Le prix se paie en mémoire. La passe avant d’un shard `KECCAK_F` de `2^18` occupe 42 GiB, et deux de ces shards en cours de traitement ont fixé le pic du bloc mesuré.

## Ce qui est délégué, et ce qui ne l’est pas

Le code des bibliothèques atteint les délégations par des copies corrigées de `k256`, `ark-ff` et `revm-precompile` : la récupération de clé secp256k1 devient du code `k256` sur `MOD_MUL` et `EC_ADD`, et un couplage BN254 devient du code `ark-bn254` sur `MOD_MUL`. Le `MULMOD` de l’EVM avec un module arbitraire, `MODEXP`, BLS12-381 et tout schéma de signature complet s’exécutent sous forme d’instructions. La prise en charge dédiée des signatures pour les programmes invités fait partie de la [trajectoire de la v2.0.0](https://apogee.gweb3networks.com/docs/quantum-leap/signatures).

La spécification : [ABI de délégation](https://apogee.gweb3networks.com/docs/auditors/spec/delegation), [Circuits de délégation](https://apogee.gweb3networks.com/docs/auditors/spec/delegation-circuits).
