Technologie
Des règles prouvées, des actes tracés, des preuves vérifiables
ProAdmin s'appuie sur VVL, un langage où une règle de gestion s'écrit comme un contrat : la fonction annonce ce qu'elle exige et ce qu'elle garantit, ces garanties sont démontrées avant sa mise en service, et chacune de ses exécutions est inscrite dans un registre signé. Cette page le montre par le code.
Le contrat
La fonction déclare ses conditions d'entrée et de sortie :
[contract].La preuve
Elles sont démontrées pour tous les cas possibles, avant le premier appel :
[contract:verify].La transaction
Chaque appel passe en entier ou pas du tout. Une règle violée n'écrit rien.
Le bloc signé
L'appel validé devient un bloc, chaîné au précédent par SHA-256 et signé en Ed25519.
La vérification
Le registre se rejoue en entier, et un fait se prouve hors du serveur :
[ledger:proof].
Une fonction qui annonce ses règles
En VVL, une fonction s'appelle un mot-clé. Lui ajouter un bloc [contract] rattaché à un registre en fait un contrat intelligent : le corps ne s'exécute que si les conditions d'entrée sont vraies, et son résultat n'est retenu que si les conditions de sortie le sont aussi.
Lire un contrat, ligne à ligne
[args]nomme les arguments, avec leur type et leurs bornes. Un appel hors bornes est refusé avant toute lecture.[ledger]désigne le registre où l'appel sera inscrit.[state]déclare les emplacements que le contrat lit et écrit. Leur clé se construit à partir des arguments : il ne peut toucher à rien d'autre.[require]: ce qui doit être vrai avant. Ici, la personne n'a pas encore consenti.[ensure]: ce qui sera vrai après. Ici, le consentement est enregistré.- Le corps fait le travail avec
[state:put]et[state:get]. S'il ne tient pas la promesse de[ensure], la transaction est annulée.
[keyword consentir
[contract
[args [sujet :type String :required]
[finalite :type Integer :min 1 :max 9]]
[ledger rgpd]
[state [c "consent:{sujet}:{finalite}"]]
[require "c = 0"]
[ensure "c = 1"]]
[do [state:put c 1]
"ok"]]
| Clause | Ce qu'elle ajoute |
|---|---|
[invariant "src >= 0"] | Une condition revérifiée après chaque transaction du contrat. |
[distinct src dst] | Deux emplacements qui ne doivent jamais être le même : on ne se vire pas de l'argent à soi-même. |
[signed cle sig "…"] | L'appel doit porter la signature Ed25519 de la personne dont la clé publique est rangée dans le registre. La signature reste dans le bloc. |
[ledger:invariant] | Une règle du registre entier, qui s'impose à tous les contrats, présents et à venir. |
Un droit d'accès est un fait inscrit, pas une ligne qu'on modifie
Dans une connexion unique, la question est toujours la même : cette personne a-t-elle le droit d'ouvrir cette application ? Ici, la réponse est un emplacement du registre qui vaut 0 ou 1, et seuls deux contrats peuvent le changer.
Accorder, retirer
- Un pseudonyme, jamais une adresse. Le sujet
p-4f1cne dit rien de la personne. Le lien entre un pseudonyme et une identité est gardé hors du registre. - Des états codés en nombres. Le service est un code, le droit vaut 0 ou 1 : ce sont des valeurs sur lesquelles une preuve peut raisonner.
- Un refus n'écrit rien. Accorder deux fois le même droit viole
[require "a = 0"]: l'appel est rejeté et aucun bloc n'est créé. - Le retrait a la même forme que l'octroi. Mêmes arguments, et il prend effet au bloc suivant.
Quand la personne signe elle-même
La clause [signed] va plus loin : la demande de connexion doit être signée par la clé de la personne. Un administrateur ne peut donc pas ouvrir une session à sa place, et la signature, conservée dans le bloc, prouve son accord. Du jeton de session, le registre ne reçoit que l'empreinte.
[ledger:create sso]
[keyword accorder
[contract
[args [sujet :type String :required]
[service :type Integer :min 1 :max 99]]
[ledger sso]
[state [a "acces:{sujet}:{service}"]]
[require "a = 0"]
[ensure "a = 1"]]
[do [state:put a 1]
"ok"]]
[keyword retirer
[contract
[args [sujet :type String :required]
[service :type Integer :min 1 :max 99]]
[ledger sso]
[state [a "acces:{sujet}:{service}"]]
[require "a = 1"]
[ensure "a = 0"]]
[do [state:put a 0]
"ok"]]
[contract:verify accorder]
[accorder "p-4f1c" 7]
[accorder "p-4f1c" 7]
[ledger:get sso "acces:p-4f1c:7"]
Réponse du serveur
{ "keyword": "accorder", "ledger": "sso",
"method": "static-symbolic",
"assumes": "the invariants hold before the call",
"status": "proved", "exit": { "a": "1" } }
ok
ContractViolation: [accorder] precondition violated: "a = 0"
(sujet="p-4f1c", service=7; a=1)
1
Le règlement, écrit comme des règles qui s'exécutent
Le RGPD demande au responsable d'un traitement de pouvoir démontrer ce qu'il fait. Un registre de contrats s'y prête : chaque consentement, chaque retrait et chaque demande est une transaction signée, et les règles sont vérifiées par la machine à chaque appel.
Consentir, retirer, et une règle pour tout le registre
- Une règle de registre.
max(consent:*) <= 1porte sur tous les emplacements dont la clé commence parconsent:. Aucun contrat, même écrit plus tard, ne peut la contourner. - La règle entre dans la preuve. Le verdict la cite dans
ledger_invariants: le contrat est démontré compatible avec elle. - Retirer est aussi simple que consentir. Les mêmes arguments, aucune condition de plus.
- L'historique est la piste d'audit. Chaque bloc garde l'appel, ses arguments et ce qu'il a écrit.
Aucune donnée personnelle dans le registre
Un registre ne s'efface pas. On n'y écrit donc que des pseudonymes, des codes et des nombres. Les données elles-mêmes vivent dans un coffre séparé, qui se rectifie et se détruit. Effacer une personne, c'est détruire son coffre et ce qui reliait son pseudonyme à elle : le registre garde la preuve que l'effacement a eu lieu, sans plus pouvoir dire de qui il s'agit.
[ledger:create rgpd]
[ledger:invariant rgpd borne "max(consent:*) <= 1"]
[keyword retirer-consentement
[contract
[args [sujet :type String :required]
[finalite :type Integer :min 1 :max 9]]
[ledger rgpd]
[state [c "consent:{sujet}:{finalite}"]]
[require "c = 1"]
[ensure "c = 0"]]
[do [state:put c 0]
"ok"]]
[contract:verify consentir]
[consentir "p-4f1c" 3]
[consentir "p-4f1c" 12]
[retirer-consentement "p-4f1c" 3]
[ledger:history rgpd]
Réponse du serveur
{ "keyword": "consentir", "ledger": "rgpd",
"method": "static-symbolic",
"ledger_invariants": ["borne"],
"status": "proved", "exit": { "c": "1" } }
ok
ContractViolation: [consentir] argument finalite (@2) = 12 > max 9
ok
[ ...
{ "height": 2, "kind": "tx",
"calls": [{ "keyword": "consentir",
"args": ["p-4f1c", 3], "result": "ok" }],
"writes": { "consent:p-4f1c:3": 1 },
"hash": "sha256:4bfb5074…" },
{ "height": 3, "kind": "tx",
"calls": [{ "keyword": "retirer-consentement",
"args": ["p-4f1c", 3], "result": "ok" }],
"writes": { "consent:p-4f1c:3": 0 },
"hash": "sha256:d2bfdaf6…" } ]
| Ce que demande le règlement | La règle dans le contrat |
|---|---|
| Démontrer le consentement (art. 7.1) | Chaque consentement est un bloc signé. L'historique de son emplacement dit quand il a été donné et quand il a été retiré. |
| Un retrait aussi simple (art. 7.3) | Le contrat de retrait prend les mêmes arguments que le consentement, sans autre condition. |
| Pas de traitement sans base légale (art. 6) | Le contrat de traitement exige c = 1. Sans consentement en vigueur, l'appel est rejeté et le refus ne laisse aucune écriture. |
| Répondre dans le mois (art. 12.3) | L'échéance est inscrite avec la demande. Une réponse tardive est enregistrée comme telle : le contrat ne sait pas la maquiller. |
| Effacer, sans retour (art. 17) | Règle de registre sum(erased:*) >= old(sum(erased:*)) : le nombre de personnes effacées ne baisse jamais. Un contrat qui voudrait réactiver quelqu'un est réfuté par la preuve, puis arrêté à l'exécution. |
| Rendre compte (art. 5.2) | [ledger:verify] rejoue tout l'historique et confirme que le registre est intact. |
Démontré pour tous les cas, pas seulement testé sur quelques-uns
Un test essaie des valeurs. La preuve raisonne sur toutes. [contract:verify] exécute le corps du contrat une fois, sur un état où chaque emplacement et chaque argument est une inconnue, puis cherche à établir que les conditions d'entrée, les bornes des arguments et les règles du registre entraînent les conditions de sortie et le maintien de ces règles.
provedVrai pour tous les états de départ et tous les arguments. Le verdict précise l'hypothèse retenue : les règles du registre tenaient avant l'appel.
refutedFaux, avec un contre-exemple : les valeurs exactes qui mettent le contrat en défaut, et la condition qui ne tient pas.
unknownNi démontré ni réfuté, avec la raison. Le prouveur ne répond jamais « prouvé » quand il ne sait pas.
Un contrat faux, et ce que la preuve en dit
Ce virement promet que la somme des deux comptes ne change pas. Son corps, lui, crédite un de trop. La preuve le réfute, et trouve en plus un cas auquel l'auteur n'avait pas pensé : quand la source et la destination sont le même compte.
- Le contre-exemple se lit tel quel : un solde de 1, un montant de 1, vers soi-même.
- Tous les cas sont examinés : emplacements distincts ou confondus. Le contrat n'est prouvé que si chacun l'est.
- La correction tient en deux lignes : retirer le « + 1 » et déclarer
[distinct src dst]. Le verdict rend alors l'état de sortie sous forme de formule.
[keyword virement
[contract
[args [de :type String]
[vers :type String]
[montant :type Integer :min 1]]
[ledger banque]
[state [src "compte:{de}" :default 0]
[dst "compte:{vers}" :default 0]]
[require "montant <= src"]
[ensure "src + dst = old(src) + old(dst)"]]
[do [state:put src ([state:get src] - @3)]
[state:put dst ([state:get dst] + @3 + 1)]
"ok"]]
[contract:verify virement]
Réponse du serveur
{ "keyword": "virement", "ledger": "banque",
"method": "static-symbolic",
"status": "refuted",
"case": "dst = src (same slot)",
"counterexample": { "dst": 1, "montant": 1 },
"cases": [
{ "case": "dst = src (same slot)", "status": "refuted" },
{ "case": "all slots distinct", "status": "refuted" } ] }
[keyword virement-juste
[contract
[args [de :type String]
[vers :type String]
[montant :type Integer :min 1]]
[ledger banque]
[state [src "compte:{de}" :default 0]
[dst "compte:{vers}" :default 0]]
[distinct src dst]
[require "montant <= src"]
[ensure "src + dst = old(src) + old(dst)"]
[invariant "src >= 0"]]
[do [state:put src ([state:get src] - @3)]
[state:put dst ([state:get dst] + @3)]
"ok"]]
[contract:verify virement-juste]
Réponse du serveur
{ "keyword": "virement-juste", "ledger": "banque",
"method": "static-symbolic",
"assumes": "the invariants hold before the call",
"status": "proved",
"exit": { "dst": "dst + montant", "src": "-montant + src" },
"cases": [
{ "case": "all slots distinct", "status": "proved" } ] }
Ce que la preuve établit, et ce qu'elle n'établit pas
- Elle porte sur la transition d'état d'un contrat : ce qu'il lit, ce qu'il écrit, et les conditions qui les relient. Elle vaut sous l'hypothèse, affichée dans le verdict, que les règles du registre tenaient avant l'appel.
- Elle raisonne sur des nombres. C'est pourquoi les états sont codés en entiers : 0 ou 1, un compteur, une échéance. Une condition qui compare des textes rend
unknown, pasproved. - Quand un contrat consulte une source extérieure, sa réponse est traitée comme une inconnue : ce qui est prouvé vaut quelle que soit cette réponse.
- Elle ne dit rien de ce qui se passe hors du registre, ni de la durée d'exécution, ni du serveur lui-même. Pour cela il y a la signature des blocs, le rejeu et les preuves de la section suivante.
Un registre qui se rejoue, des faits qui se prouvent ailleurs
Une trace n'a de valeur que si celui qui la tient ne peut pas la réécrire en silence. Trois mécanismes y veillent : le chaînage et la signature de chaque bloc, le rejeu complet du registre, et la preuve qu'un tiers vérifie sans y avoir accès.
Ce que contient un bloc
- L'appel : le contrat, ses arguments, son résultat, et l'empreinte du code qui a tourné. On sait quelle version de la règle s'est appliquée.
- Les lectures et les écritures : ce que le contrat a vu et ce qu'il a changé.
- La racine d'état : l'empreinte, par un arbre de Merkle, de tous les emplacements du registre après ce bloc.
- L'empreinte du bloc précédent : modifier un bloc ancien casse tous les suivants.
- La signature Ed25519 du bloc.
Rejouer
[ledger:verify] contrôle le chaînage et les signatures, puis exécute de nouveau chaque transaction avec le code enregistré. Chaque bloc doit redonner les mêmes lectures, les mêmes écritures et la même racine d'état.
Prouver hors du serveur
[ledger:proof] rend une preuve autonome : la valeur d'un emplacement, son chemin dans l'arbre de Merkle, le bloc de tête et sa signature. Un auditeur la vérifie sans interroger le registre. L'absence se prouve aussi : « cette personne n'a jamais consenti à cela ». La clé publique voyage avec la preuve : le vérificateur compare son empreinte à celle qu'il a obtenue par un autre canal.
Pour dater le registre indépendamment de celui qui l'exploite, sa tête peut être ancrée auprès d'un service d'horodatage (RFC 3161) ou par OpenTimestamps.
[ledger:block rgpd 2]
Réponse du serveur
{ "height": 2, "kind": "tx",
"prev_hash": "sha256:689c5901…",
"calls": [{ "keyword": "consentir",
"code_hash": "sha256:e30597ab…",
"args": ["p-4f1c", 3], "result": "ok" }],
"reads": { "consent:p-4f1c:3": { "v": null, "value": null } },
"writes": { "consent:p-4f1c:3": 1 },
"state_root": "sha256:7522c1a3…",
"hash": "sha256:4bfb5074…",
"signatures": [{ "alg": "ed25519", "key": "server",
"sig": "OZ7t+OQaN36Hus1F…" }] }
[ledger:verify rgpd]
$preuve = [ledger:proof rgpd "consent:p-9a20:5"]
[ledger:check-proof $preuve]
[ledger:check-proof [ledger:proof rgpd "consent:inconnu:1"]]
Réponse du serveur
{ "ledger": "rgpd", "ok": true, "height": 3,
"blocks": 4, "signed": 4, "unsigned": 0,
"replayed_calls": 2, "mode": "replay", "error": null }
{ "ok": true, "slot": "consent:p-9a20:5", "height": 4,
"reason": "present", "present": true, "value": 1,
"signed": true }
{ "ok": true, "slot": "consent:inconnu:1", "height": 4,
"reason": "absent", "present": false,
"signed": true }
Rendre une fonction traçable et prouvable, en six gestes
La méthode ne dépend pas du sujet. Un droit d'accès, un consentement, un stock, une validation de facture : toute règle qui fait passer un état d'une valeur à une autre s'écrit de cette façon.
Coder l'état en nombres
Donnez un nom à chaque emplacement (
acces:{sujet}:{service}) et une valeur entière à chaque état : 0 ou 1, un compteur, une échéance. Les textes, eux, restent hors des conditions.Borner les arguments
Type, minimum, maximum, liste de valeurs admises. Ces bornes sont vérifiées à chaque appel et servent d'hypothèses à la preuve.
Écrire ce qui est exigé et ce qui est garanti
[require]pour l'avant,[ensure]pour l'après,old(…)pour comparer les deux. Ce qui doit valoir pour tout le registre devient une règle[ledger:invariant].Garder un corps rejouable
Un contrat doit redonner le même résultat quand on le rejoue. Un tirage aléatoire ordinaire y est refusé : le registre fournit son heure,
[ledger:time], et son tirage,[ledger:random], qui se rejouent à l'identique.Prouver avant de servir
Lancez
[contract:verify]. Un verdictrefuteddonne le contre-exemple à corriger ; un verdictunknowndit quelle condition reformuler. On ne met en service qu'un contratproved.Laisser le registre témoigner
Chaque appel est désormais un bloc signé.
[ledger:history]donne la piste d'audit,[ledger:verify]la rejoue,[ledger:proof]en extrait une preuve à remettre à un tiers.
Où en est ProAdmin
- Les contrats, la preuve, le registre signé, le rejeu et les preuves de Merkle présentés ici existent dans le serveur VAILS. Les extraits de cette page y ont été exécutés ; les réponses sont reproduites telles quelles, abrégées quand elles sont longues.
- L'espace client de ce site est aujourd'hui une démonstration : le raccordement des comptes ProAdmin à ces registres est en cours.
Le langage derrière ProAdmin
VVL se découvre et s'essaie en ligne, avec sa documentation et ses exemples.