Architecture
Le compagnon d'ingénierie du livre blanc — comment le nœud de référence réalise la conception, et les décisions qui la sous-tendent. Explicatif, non normatif.
Le protocole, ce sont les fichiers de paramètres et les vecteurs de référence, plus les règles nommées là où elles sont définies ; la spécification réseau porte les exigences de la couche pair. Cette page explique cette surface et consigne les décisions qui la sous-tendent. Là où elle diverge de la surface normative, c'est la surface normative qui l'emporte et ce texte est corrigé. Le compagnon d'ingénierie complet est docs/ARCHITECTURE.md.
Deux prédicats, deux moteurs#
La vie d'un certificat, de bout en bout :
wallet or sequencer network every full node
+-------------+ gossip +--------------+ +---------------------+
| build cert | ---------> | cert topic | ------------> | STATELESS PIPELINE |
| (reads, | | (TLS gossip) | | V-rules, strict |
| writes, | +--------------+ | Ed25519, program |
| sigs, fee, | | re-execution |
| deposit) | | -> mark VALID |
+-------------+ +----------+----------+
| mempool
v
miner (any node) +---------------------+
+----------------------+ block | FOLD (sequential) |
| order the hash list, | ---------> | per certificate: |
| append forced certs, | gossip | APPLIED / SKIPPED / |
| RandomX solve | | FORCED_FAILED / |
+----------------------+ | DROPPED -> state |
+---------------------+
La validité est sans état et parallèle : elle s'évalue une fois par certificat et par nœud, avant les blocs et indépendamment d'eux. L'applicabilité est avec état et séquentielle : elle s'évalue à l'intérieur du fold, à la position où le certificat est inscrit. Le mineur n'exécute rien de ce qu'on lui donne : il ordonne des hashs qu'il a déjà vus validés et résout la preuve de travail ou, après la bascule vers la preuve d'enjeu dans l'ère 2, signe le slot pour lequel il a été tiré au sort. Sa seule part d'exécution est celle du chemin forcé, à partir de l'ère 1 : il peut exécuter des intents ouverts contre l'état de fin de son bloc et en ajouter les résultats, 16 au plus par bloc, que chaque nœud contrôle ensuite comme n'importe quel autre certificat. Le fold est une boucle serrée sur un ensemble de travail en mémoire — comparer, additionner, écrire — et n'exécute absolument rien.
Principes d'ingénierie#
| Principe | |
|---|---|
| P1 | Le fold est sacré. La fonction de transition d'état vit dans un unique paquet pur, sans entrées-sorties, sans horloges, sans goroutines, sans itération sur des tables associatives et sans virgule flottante. C'est le seul code dont les bugs sont irréparables après coup. Tout le reste du nœud n'est que de la plomberie remplaçable. |
| P2 | Le déterminisme prime sur la performance. Toute optimisation qui risque d'introduire du non-déterminisme est rejetée dans le code de consensus. La performance a sa place dans le pipeline sans état, où elle est sans danger. |
| P3 | Aucune clé d'administration, aucun RPC privilégié. Il n'existe aucun chemin de code par lequel une clé pourrait suspendre, mettre à jour, émettre ou réorganiser. Si ce n'est pas dans les règles du fold, cela n'existe pas. |
| P4 | Surface de consensus réduite. core/ n'importe rien en dehors de la bibliothèque standard. Le reste du nœud peut utiliser des bibliothèques de l'écosystème ; le cœur ne le peut pas. |
| P5 | La spécification d'abord. Les vecteurs de référence sont le protocole. Le code Go est une implémentation de référence ; une implémentation indépendante qui passe les vecteurs est un pair, pas un fork. C'est ce qui permet au mainteneur de finir par n'être personne en particulier. |
| P6 | Reproductible depuis la v0.1. Chaîne d'outils Go fixée, -trimpath, dépendances fixées. La confiance passe du binaire au code, seule confiance qu'un auteur anonyme puisse offrir. |
| P7 | Un seul binaire, ouvert par la hauteur. La machine, la mécanique des séquenceurs et le chemin forcé sont compilés dans chaque version et refusés en dessous de leur hauteur d'activation, h1_vm (80 640 sur le mainnet), dont Validate exige qu'elle tombe sur une frontière d'époque. L'Ère 1 arrive parce que la chaîne a atteint un nombre, jamais parce qu'on a demandé aux opérateurs de mettre à jour. Les ères ultérieures ne sont pas dans le binaire : chacune est livrée dans sa propre version, par hard fork. |
P3 est celle qu'un lecteur sceptique devrait presser le plus fort, et c'est sur la trésorerie qu'il faut la presser : la genèse ne contient aucune clé ni aucun chemin de dépense, il n'y a donc aucun privilège à détenir, déléguer ou voler. La trésorerie ne peut pas être dépensée avant la hauteur 259 200, et quelle que soit la règle qui l'ouvrira, elle arrivera comme un changement de consensus — le même mécanisme social que tout autre, soumis au même refus, et capable de déplacer exactement une cellule même alors. La part du fondateur est un crédit, non une clé : 3 % de chaque subvention vers une adresse fixée dans le fichier de paramètres, jusqu'à la hauteur 3 153 600, que l'ère 2 lit comme l'époque 1 095. Cette adresse détient des pièces et n'atteint rien d'autre. Une clé d'administration est une clé qui existe avant que quiconque y ait consenti et qui atteint tout.
Primitives cryptographiques#
| Rôle | Choix | Pourquoi |
|---|---|---|
| Signatures | Ed25519 | Vérification par lots (la voie d'accès aux GPU), pas de malléabilité, clés minuscules. Règles strictes fixées à la genèse : encodages canoniques exigés pour la clé publique et R, clé publique sans torsion et pas d'ordre faible, et vérification sans cofacteur. |
| Hachage | BLAKE3 | Identifiants de certificats et de blocs, adresses, racine d'état. Assez rapide pour hacher au débit du relais ; adapté au parallélisme pour la racine d'état d'époque. |
| Preuve de travail | RandomX | Optimisée pour les processeurs. Le seul cgo de l'arborescence, derrière un tag de build, et absent d'un build qui ne l'a pas. pow_engine est dans la racine de consensus, si bien qu'un binaire portant le mauvais moteur refuse de démarrer plutôt que d'accepter la mauvaise preuve. |
| Séparation des domaines | obligatoire | Chaque empreinte est blake3(tag ‖ payload). Les signatures signent sur l'identifiant de chaîne et la racine de consensus, ce qui tue le rejeu à la fois entre réseaux et entre deux incarnations d'un même réseau. |
Le rejet de la torsion est ce qui rend sûr le chemin par lots. Une clé d'ordre mixte n'est pas d'ordre petit, aucune liste de blocage ne l'atteint donc, et c'est exactement là qu'un vérificateur par lots avec cofacteur et un vérificateur unitaire sans cofacteur divergent. Avec la clé et R dans le sous-groupe d'ordre premier, les deux sont prouvablement équivalents — un vérificateur par lots peut donc utiliser le cofacteur à condition d'appliquer les mêmes règles d'encodage et de torsion avant de constituer le lot. Cette obligation est le prix de ce choix, et le vérificateur par lots n'existe pas encore.
Encodage canonique et identifiants#
Tous les objets de consensus sont des conteneurs SSZ : un unique encodage canonique en octets, pas d'ordre de table de hachage, pas d'ambiguïté de champ optionnel, des décalages fixes pour un décodage partiel bon marché, et une merkleisation native.
cert_id = blake3("zcd/certid/v1" || ssz(certificate with an empty signature list))
cert_exemplar = blake3("zcd/cert/v1" || ssz(certificate))
block_id = blake3("zcd/block/v1" || ssz(header))
Les deux premiers sont des empreintes différentes répondant à des questions différentes, et une implémentation qui emploie l'une là où l'autre a sa place est monétairement cassée. L'identifiant répond à cette autorisation a-t-elle été facturée ; le hachage d'exemplaire répond à ces octets le prouvent-ils. Les deux ne partagent jamais une clé.
Les signatures sont hors de la préimage de l'identifiant parce qu'une signature est une démonstration randomisée : le signataire choisit le nonce, chaque nonce donne une autre signature valide et parfaitement canonique sur le même corps, et aucun vérificateur ne peut savoir lequel a servi. Si elles étaient dedans, une autorisation aurait une infinité d'identifiants, chacun facturable, chacun produisible par n'importe lequel de ses signataires requis seul, aux dépens de l'autorité des autres.
Une enchère hors de l'identifiant serait une enchère que n'importe qui en transit pourrait réécrire — gonflée pour brûler le solde du signataire par les frais de base, ou mise à zéro pour tenir le certificat hors de tout bloc. Ce qu'un certificat paie fait partie de ce que son signataire a autorisé : c'est donc haché et c'est signé. Un changement d'encodage ultérieur qui « sortirait les frais du corps signé pour souplesse de relais » aurait l'air d'une optimisation et serait un vecteur de vol.
La règle qui en découle : décoder, ne pas valider deux fois. Le décodage impose la forme canonique, si bien qu'un objet décodé est structurellement valide par construction et que les moteurs de règles ne revérifient jamais la forme.
Le modèle de cellules#
Une cellule est la valeur à un slot. Les valeurs de cellules sont des entiers non signés de 256 bits stockés en gros-boutien sur 32 octets. Les cellules absentes se lisent comme zéro — zéro est absence, ce qui est une exigence de consensus plutôt qu'une commodité d'implémentation : cela garde la racine d'état fonction de l'état plutôt que de l'historique qui l'a produit.
Séparément, le protocole tient un registre des adresses dépensées : un ensemble de consensus permanent d'adresses à usage unique dont l'autorité de signature a été brûlée.
Addr = version || blake3("zcd/addr/v1" || version || payload)[:31]
| Version | Type | Autorisation de débit |
|---|---|---|
0x01 | utilisateur à usage unique | Signature du propriétaire ; tout certificat qui débite doit aussi porter un MARK_SPENT explicite. Une fois appliqué, toute lecture et toute écriture sous cette adresse échouent pour toujours. |
0x02 | utilisateur persistant | Signature du propriétaire ; réutilisable pour toujours. Ne peut jamais entrer dans le registre des adresses dépensées. |
0x03 | actif | Régie par les cellules d'autorité immuables de l'actif. |
0x00 | protocole | Réservée au fold : balise d'époque, cellules de frais de base, anneau de maturité du coinbase, cellule de trésorerie, compteurs des séquenceurs. |
0x05 | contrat | Personne par clé : aucune clé ne se hache vers une telle adresse. Le code du contrat et les règles du fold décident. |
0x04 | non défini | Aucune cellule n'existe sous cette version ; un certificat qui nomme une telle adresse est invalide. |
L'accès typé est l'origine du parallélisme. Une lecture déclare comment elle
contraint sa cellule — EXACT, ou l'une des gardes GUARD_GE,
GUARD_LE, GUARD_EQ — et une écriture déclare ce qu'elle fait :
SET, DELTA_ADD, DELTA_SUB ou MARK_SPENT. Seul
EXACT alimente le calcul, et seul EXACT devient périmé quand la cellule bouge ;
une garde ne renvoie rien et un delta ne lit rien, si bien que la ré-exécution reste une fonction pure des
lectures exactes et que n'importe quel nombre de débits et de crédits gardés sur un même slot commutent. Une seule discipline par
slot et par certificat : un set et un delta ne partagent jamais un slot. Écrire zéro supprime la cellule, si bien qu'un
slot jamais écrit et un slot écrit à zéro sont le même slot.
Usage unique ou persistant est une politique de portefeuille exposée par un bit d'adresse, non deux registres. Les portefeuilles utilisent par défaut une nouvelle adresse à usage unique par paiement entrant, la voie sans contention ; quiconque veut un compte utilise la version persistante. Les récompenses de minage doivent aller sur une adresse persistante : le consensus accepte l'un ou l'autre type comme destination de paiement, mais une adresse à usage unique brûle chaque récompense qu'elle détient dès qu'elle est dépensée, si bien que le nœud en refuse une comme adresse de paiement.
L'entrée du registre n'est jamais compactée. Les valeurs de cellules sous une adresse dépensée peuvent être élaguées après l'horizon d'annulation, mais l'entrée qui consigne l'adresse comme dépensée est ce qui l'empêche d'être ressuscitée. C'est un état de consensus en ajout seul, environ 33 octets par adresse, pour toujours — le problème ouvert que le protocole assume, partagé structurellement avec toute conception à ensemble de nullificateurs.
Validité sans état, et la loi de facturation#
Les règles V s'exécutent sur chaque certificat, en parallèle, avant l'admission au mempool et pendant la vérification des blocs, et n'exigent aucun état : forme canonique et identifiant de chaîne, chaque liste restant dans sa limite ; chaque signature vérifiée en Ed25519 strict sur la racine de signature, l'ensemble des signatures étant minimal autant que suffisant ; la ré-exécution du programme contre les lectures déclarées produisant exactement les écritures déclarées ; une autorisation pour chaque écriture qui en exige une, et une signature sur la cellule de dépôt sans condition ; un dépôt qui est le solde natif d'une adresse d'utilisateur et qui couvre le pire cas du certificat ; la cohérence interne, de sorte qu'aucune écriture ne contredise une lecture du même slot et qu'aucune adresse à usage unique débitée n'échappe au marquage comme dépensée ; aucune écriture dans une cellule de protocole ; un octet de version défini sur chaque adresse nommée ; et, dans un contrat dont la clé de séquenceur est non nulle, la signature de cette clé sur le chemin exact.
La loi de facturation du système tient en une phrase, et c'est ce à quoi il faut se tenir :
Une signature, au plus une facture, jamais à une position que son signataire ne pouvait pas éviter.
Le fold ordonne un bloc en trois classes et ne laisse aucune latitude au producteur : d'abord les
fills, dans l'ordre des pipelines de séquenceur auxquels ils appartiennent ; puis tous les autres
certificats par (deposit address, Seq, id) ; enfin les certificats
forcés, par identifiant. Chaque certificat a alors exactement une issue parmi quatre, et les
vecteurs de référence les nomment :
| Issue | Quand | Qui paie |
|---|---|---|
APPLIED | Chaque lecture déclarée tient. | Le déposant, au coût réel : frais de base brûlés, frais de priorité au producteur. |
SKIPPED_STALE, SKIPPED_OVERFLOW | Une lecture ne tient plus, ou un delta sortirait de [0, 2²⁵⁶). Rien n'est écrit. | Le déposant : des frais d'omission fixes, brûlés. Pour un fill, un séquenceur — jamais l'utilisateur. |
FORCED_FAILED | Un certificat forcé dont l'appel a échoué (revert) ou a enfreint ses bornes. | Ses payeurs ; rien d'autre n'est écrit. |
DROPPED | Le dépôt ne peut pas être réservé : la cellule n'est pas approvisionnée ou est dépensée. | Personne. Non marqué comme vu, il peut donc être soumis à nouveau contre un dépôt approvisionné. |
Les certificats appliqués, omis et en échec forcé sont tous marqués comme vus, et inclure un certificat vu, expiré ou sous-enchéri rend le bloc invalide, si bien qu'aucun producteur ne peut ré-inclure ou retarder le certificat d'autrui pour en récolter le dépôt. Les échecs qui n'ont personne à facturer — un fill dont le compteur de pipeline est hors d'ordre ou dont le compteur fence a bougé, un certificat forcé dont les lectures ne tiennent pas — rendent au contraire le bloc invalide : le producteur, qui a construit le bloc à partir d'un état qu'il détient, est la partie qui aurait pu le savoir.
Portes d'ère#
Les hauteurs d'activation se trouvent dans spec/params.json. h1_vm = 80 640 ouvre
l'ère 1 : la cEVM et la bibliothèque pré-déployée, les enregistrements de contrat avec une clé de séquenceur, un administrateur et un
compteur fence, les intents et les fills, l'annuaire et le chemin forcé. Les constantes propres à l'ère
— la fenêtre d'intent de 8 blocs, 16 certificats forcés par bloc à 4 000 unités de dérivation chacun,
une durée de vie de l'entrée d'annuaire de 259 200 blocs au-delà du TTL de son certificat d'annonce — sont compilées plutôt que portées par le fichier
de paramètres, parce qu'ajouter un paramètre déplacerait l'identifiant de genèse de chaque réseau en service ; le bloc de la porte
les écrit dans des cellules de protocole, si bien que la racine d'état s'engage sur elles à partir de là.
h1_bond est égal à h1_vm sur le mainnet et sur le testnet public, si bien que les
opérations d'engagement ne s'y ouvrent jamais : il n'y a pas d'engagement d'enjeu dans l'ère 1. L'arbre porte encore les
règles de garantie (underwriting), de baux et de file forcée de la v1.0, et elles ne sont valides que sur le réseau de
développement, dans une fenêtre de 256 blocs au-dessus de son h1_vm et en dessous de sa propre porte v1, qui existe
pour que les vecteurs qui consignent cette mécanique conservent un réseau sur lequel elle est valide. h2_pos = 1 576 800 est réservé à la
preuve d'enjeu, figé à la genèse et lu par aucune règle. L'ère 2, la preuve d'enjeu, sort comme une version
à part entière par hard fork, avec ses constantes compilées comme celles de l'ère 1 : elle ouvre l'engagement à l'époque 182 par
hauteur, démarre sa rampe lorsque l'enjeu actif détient 10 % de l'offre pendant 30 époques, ou à l'époque 273 au plus
tard, et bascule vers des slots de 5 secondes lorsque la rampe s'achève. À partir de la bascule, elle lit chaque
constante de genèse qui est en réalité une durée dans l'unité qu'elle mesure, si bien que le fichier de paramètres ne
change pas.
Organisation du dépôt#
zycord/
spec/ parameter sets, golden vectors, library images <- THE PROTOCOL
core/ consensus-critical; standard library only (P4)
types/ crypto/ ssz/ u256/ state/ validity/ fold/ params/ genesis/
cevm/ the certificate-adapted EVM; vendored interpreter, pure Go
stdlib/ the pre-deployed library, its addresses and code hashes
pow/randomx/ the mainnet engine: vendored C++, cgo, behind a build tag
node/ storage/ chain/ verify/ mempool/ miner/ p2p/ sync/ rpc/ stratum/
wallet/ key management, certificate builders (reference; not consensus)
contracts/ the reference contracts, in Solidity
sim/ simulator, fuzz harnesses, differential refold, chaos soak
cmd/ zycordd, zcd
desktop/ the wallet in a native window — a separate Go module
docs/ architecture, protocol, operating guide, whitepaper
Les flèches de dépendance ne pointent que vers l'intérieur — node → core,
wallet → core, jamais l'inverse — et rien à l'intérieur de core/
n'atteint quoi que ce soit hors de core/ et de la bibliothèque standard. Une exception, nommée et imposée :
core/pow/randomx est le seul cgo de l'arbre, et il ne compile que sous le tag de
compilation, si bien que toute compilation sans le tag reste limitée à la bibliothèque standard et n'a besoin d'aucune chaîne d'outils C. La CI
exécute la vérification qui l'impose, car la vérification des dépendances tierces filtre des chemins de modules et cgo n'en
a pas — ce qui a laissé la règle imposée par personne jusqu'à ce qu'elle soit ajoutée.
Comment c'est testé#
- Vecteurs de référence. Chaque règle de fold, de bloc et de validité a des cas positifs et négatifs sous la forme
(pre-state, block) → (post-state | invalid, outcomes, fees). La suite est le contrat de compatibilité pour les implémentations indépendantes. - La suite de nuisance. Réinclusion de certificats appliqués et omis, inclusion expirée, inclusion sous-enchérie, chaînes dépendantes sous brassage du proposeur, cycles brûlage-remboursement, tempêtes de crédits par des tiers, courses aux bornes du plafond d'émission.
- Tests par propriétés. Déterminisme du fold sous permutation de l'ordre du proposeur, commutativité des deltas, tolérance à l'ABA, et conservation — y compris les dépôts en vol, l'anneau de maturité et la cellule de trésorerie, puisqu'une vérification de conservation qui omet la trésorerie signale chaque bloc comme créant de la valeur.
- Tests différentiels. Une seconde implémentation du fold délibérément naïve, écrite pour l'évidence plutôt que pour la vitesse, soumise au fuzzing contre la vraie. Une divergence bloque une publication.
- Simulation adverse. Tempêtes d'omissions, courses au drainage de dépôts, mineurs bourrant de rejets, torture par réorganisation, manipulation d'horodatages, scénarios de relais quasi éclipse. Les configurations de scénarios sont versionnées ; les exécutions sont reproductibles à partir de leur graine.
- Concurrence, délibérément. Chaque composant touché par plus d'une goroutine a un test qui est concurrent, dans la forme que le processus utilise réellement.
-racesur une suite mono-goroutine ne mesure rien et annonce un succès. - Le test d'endurance sous chaos. De vrais processus de nœuds sur de vrais sockets derrière un proxy injectant latence, gigue, pertes et partitions, avec des nœuds tués par
SIGKILLau hasard. C'est cette surface qui a trouvé la course de données que toute la suite-raceavait manquée.
La genèse est un artefact, pas une cérémonie#
zcd genesis émet le bloc de genèse — état vide, cellules de balise initialisées,
registre des dépensées vide, aucune allocation d'aucune sorte — et son identifiant. Le lancement
s'est engagé à l'avance sur le tag de code, le hachage des paramètres, l'identifiant de genèse et l'heure de lancement, et
le jeu de paramètres portait un engagement sur un message de lancement dont le bloc 1 devait révéler l'empreinte, si bien que
personne ne pouvait miner la chaîne avant son démarrage annoncé. N'importe qui peut reconstruire les quatre premiers en
quelques millisecondes, depuis les sources, sur n'importe quelle machine. Il n'y a rien d'autre à croire sur parole.