TL;DR

Cet article présente une implémentation pratique de la vie associative dans le contexte Covid-19. La technologie sous-jacente utilisée est une blockchain privée de type Ethereum dont les aspects réglementaires et sécuritaires sont discutés fonctionnellement et techniquement. L’objectif est de fournir une alternative digitale sûre pour la gestion des associations et plus spécifiquement des syndicats de copropriété.

Quelques éléments de contexte

Cet article est la suite La blockchain au service de la digitalisation de la vie associative - La communauté des curieux et vise à présenter et expliquer techniquement et fonctionnellement les contrats intelligents mis en oeuvre pour digitaliser la gestion de la copropriété.

Pour revenir au premier volet de cette série et comprendre comment utiliser cette application de blockchain comme un citoyen lambda, il faut choisir le chemin 1. du paragraphe suivant. Pour aller au troisième volet de cette série et apprendre à déployer l’infrastructure blockchain comme un sysadmin, choisissez le chemin 3.

La blockchain dont vous êtes le héros

A partir de maintenant, je vous propose trois chemins de technicité croissante selon ce que vous êtes venu chercher sur cet article.

  1. Je ne suis pas du tout un technicien et je veux faire partie de la copropriété de la communauté des Curieux. Suivez ce chemin
  2. J’ai des connaissances techniques et je souhaite les approfondir en comprenant comment cette copropriété est construite et les bases technologiques derrière. Vous êtes sur le bon chemin, keep going
  3. J’ai de bonnes connaissances d’administration système et je veux contribuer au réseau qui est construit. Suivez ce chemin

Resume

Comment s’implémentent les fonctionnalités d’une association dans blockchain Ethereum ?

La brique de base pour développer une application dans la blockchain sera ici le smart contract. Celui-ci nous permet de définir les règles de gestion afin de faire vivre notre modélisation dont le modèle de données est automatiquement stocké dans la blockchain.

Outillage

Le développeur sur Ethereum dispose des outils suivants (que j’utilise personnellement sur la distribution Ubuntu) :

  1. Visual Studio Code : Éditeur de code permettant de développer ses contrats avec la language Solidity. Des extensions existent et j’utilise personnellement celle de Juan Blanco avec plus de 200k téléchargements.
  2. Node & npm : Pour pouvoir compiler du solidity, il faut utiliser node.js et le package solc
  3. Ganache : Ce package va nous permettre de simuler une mini blockchain de test dans notre espace de développement et pouvoir tester dessus nos contrats
  4. Truffle : Ce package sert à pouvoir tester unitairement nos contrats et vérifier qu’ils se comportent correctement comme par exemple empêcher un non-membre de voter ou empêcher une personne arbitraire de rejoindre l’association sans cooptation

Des instructions détaillées se trouvent sur la page github du code.

Les smart contracts solitidy sont grosso modo des objets assez classiques, similaires à des classes javacript ou C++. Ils contiennent :

  • des structures de données pour décrire l’état interne
/// The member list
mapping (address => bool) public members;
/// The member count (easier to store than to compute each time)
uint public membersCount;
/// A contract owner, which can be seen as a technical administrator
/// but it depends on the defined contract behavior
address payable public owner;
  • ces structures de données sont plus ou moins accessibles selon le niveau de visibilité.
/// This boolean value can be retrieved using an implicit getter
bool public maintenanceMode;
/// This variable is by default internal which makes it only
/// visible inside the contract itself or any contract derived from it
mapping (address => uint) scores;
  • un constructeur pour instancier l’objet
constructor() {
   owner = msg.sender;
   members[msg.sender] = true;
   scores[msg.sender] = 90;
   lastScoreUpdate[msg.sender] = block.number;
   membersCount = 1;
}

Celui-ci récupère du message ou transaction ethereum (ici msg) des informations qu’il va utiliser pour instancier les variables et offrir l’opportunité à toute personne d’interagir avec le contrat.

Lorsque le réseau Ethereum est en place (voire le volet 3 de la série d’article) et que tout a été testé et vérifié, le développeur de l’application instancie le code dans la blockchain à travers des appels particuliers que nous verrons dans l’utilisation de la librairie web3js. Ce code est constitué de contrats qui ont des fonctions appelables par toute personne avec toujours la même librairie. Ces fonctions sont des interfaces en général toutes protégées par des modifiers qu’il faut définir :

  • onlyMembers : ne permet qu’à un membre d’appeler cette fonction
  • onlyOwner : ne permet qu’au propriétaire du contrat d’appeler cette fonction. Le owner a été défini comme l’administrateur du contrat, à savoir la personne qui instancie celui-ci. Ce rôle est dédié à des actions techniques de maintenance, de suppression ou d’action de régulation
  • maintenanceOff : ne permet à cette fonction d’être appelée que si le mode maintenance a été désactivé. Parfois, il faut désactiver une ou plusieurs fonctions pendant une mise à jour des contrats ou pendant un vote.

Ces modifiers viennent agrémenter les fonctions de la manière suivante :

/// The modifier description
modifier onlyOwner() {
   require(
       msg.sender == owner,
       "Only owner is allowed to call this"
   );
   _;
}
/// In case of a smart contract problem, activate maintenance mode
function switchMaintenanceMode() public onlyOwner {
   maintenanceMode = !maintenanceMode;
}

Prenons à présent un peu de hauteur. Chaque smart contract aura un rôle défini dans notre organisation. Commençons par décrire les données du smart contract AssociationOrg qui gère l’état de l’association :

Nous avons tout d’abord un owner, un nom et des membres. Ces trois instances sont décrites par respectivement une adresse (donc une clé publique), une string qui contient le nom de l’association et un mapping d’adresse (soit une forme de liste de clé publiques). Ces trois champs seront remplis au fur et à mesure de la vie du contrat. L’owner ici sera le président de l’association et dispose de privilèges importants sur l’administration du contrat.

contract AssociationOrg {
   address payable public owner;
   string public name;
   mapping (address => bool) public members;
   ...
}

ETAPE 1

Une personne décide d’appeler la fonction de création du contrat AssociationOrg. C’est une instanciation d’objet. Cette personne devient de facto l’owner du contrat/président de l’association et il est son unique membre comme décrit dans le constructeur. Le contrat ainsi créé renvoie une adresse unique qui doit être communiquée à tous ceux qui veulent participer à l’association.

Je ne décris pas le contrat dans le détail mais vous pouvez le trouver ici

Nous avons ensuite un deuxième contrat AssociationCoopt qui gère la cooptation dans l’association. Celui-ci contient une variable proposedMember qui est la personne proposée par un membre et une variable contenant la liste (sous forme de mapping) de tous les membres qui soutiennent cette cooptation. Chaque instance de ce contrat est une proposition de cooptation pour quelqu’un. Notons qu’un système d’héritage permet de factoriser ces variables et plusieurs fonctions sur d’autres actes d’administration.

contract AssociationCoopt {
   address payable public proposedMember;
   mapping (address => bool) public didVotes;
   uint public voteCount;
 
   /// Called by the person who wants to be coopted
   constructor(address _assoCtr) {
       proposedMember = msg.sender;
       assoCtr = AssociationOrg(_assoCtr);
   }
   ...
}

ETAPE 2

Une personne (non membre) décide d’appeler le constructeur d’un contrat de cooptation AssociationCoopt pour être coopté avec en paramètre l’adresse du contrat représentant l’association afin de faire le lien. Cette personne devient automatiquement le proposedMember du contrat. Elle récupère l’adresse de ce contrat et va l’envoyer à des amis membres de l’association pour qu’il soit coopté.

Le contrat de cooptation dispose d’une fonction vote qui appelable par toute personne qui est membre (comme décrit dans le modifier ci-dessous) de l’AssociationCoopt et qui dispose de l’adresse du contrat de cooptation créé. Quand la fonction est appelée, le contrat de cooptation incrémente le compte pour le proposedMember.

contract AssociationCoopt {
   ...
   /// Vote for the administration action
   function vote() public onlyMembers {
       if (!didVotes[msg.sender]) {
           didVotes[msg.sender] = true;
           voteCount ++;
       }
   }
   ...
}

Le plus important est d’avoir le maximum de cooptation et donc d’incrémenter voteCount. Cette variable permettra de transformer la cooptation en une adhésion.

Le code complet se trouve ici.

Pour formaliser l’adhésion, à tout moment, toute personne qui appelle la fonction handleCooptationAction du contrat d’association d’origine AssociationOrg (et pas du contrat de cooptation) en fournissant l’adresse du contrat de cooptation pour acter cette cooptation. Évidemment, cette dernière suit des règles décrites dans le contrat. J’en ai décrite une “Il faut une cooptation d’au moins 51% des membres” mais d’autres règles sont possibles comme présenté ici dans les require.

contract AssociationOrg {
   ...
   function handleCooptationAction(address payable _adminCtr) public maintenanceOff {
       AssociationAdministrationCooptation assoAdminCtr = AssociationAdministrationCooptation(_adminCtr);
       require(assoAdminCtr.adminAction() == AssociationAdministration.AdminAction.COOPTATION, "Cooptation action required");
       require(assoAdminCtr.assoCtr() == this, "Invalid AdministrationContract reference to MoroccanContract");
       require(assoAdminCtr.voteCount() > membersCount/2, "Need at least 51% of the votes");
       require(!members[assoAdminCtr.proposedMember()], "Already a member");
       members[assoAdminCtr.proposedMember()] = true;
       scores[assoAdminCtr.proposedMember()] = 90;
       lastScoreUpdate[assoAdminCtr.proposedMember()] = block.number;
       membersCount ++;
       seenAdmins[_adminCtr] = true;
   }
   ...
}

ETAPE 3

La cooptation est actée à travers l’appel à la fonction ci-dessus. Le nouveau membre est automatiquement ajouté à l’association AssociationOrg. La blockchain se met à jour automatiquement pour prendre acte de la décision collégiale.

Prenons une pause à ce stade pour comprendre ce qu’il s’est passé

  1. L’association existe dans la blockchain à travers les structures de données hébergées dans un contrat d’association :
    • owner : c’est la personne qui crée le contrat et devient de facto le président
    • members : Ce sont les membres de l’association qui ont été cooptés
  2. L’ajout d’un membre se fait par la création d’un contrat de cooptation qui contient les données :
    • proposedMember : C’est l’adresse de la personne que nous désirons coopter
    • didVotes : C’est la liste des cooptations reçues par la personne
    • voteCount : C’est le nombre de cooptation reçu, il doit être supérieur à un seuil pour que la cooptation devienne une adhésion
  3. Il est possible d’interagir avec ces structures à travers des fonctions :
    • Le constructeur du contrat d’association : c’est la création d’une association et ne se fait qu’une seule fois par association
    • Le constructeur du contrat de cooptation : se fait par et pour chaque personne qui veut être coopté dans une association définie
    • La fonction vote du contrat de cooptation : appelée par le membre d’une association pour voter pour l’adhésion de la personne dans l’association du contrat de cooptation
    • La fonction handleCooptationAction du contrat d’association : appelée par n’importe qui pour finaliser l’adhésion et vérifier si le nombre de cooptation est suffisant
  4. Ces fonctions sont controlées par des règles définies
    • onlyOwner : Ne peut être appelée que par le président (dissolution par exemple)
    • onlyMember : Ne peut être appelée que par un membre (vote sur la cooptation d’un membre)
  5. Le lien entre les contrats se fait à travers des adresses ou références
    • Pour créer un contrat de cooptation, il faut fournir au contrat de cooptation l’adresse du contrat d’association
    • Pour finaliser l’adhésion d’une personne, il faut fournir l’adresse du contrat de cooptation au contrat d’association

Reprenons le fil de nos étapes. Supposons qu’à présent, l’owner de l’association doive changer pour une raison quelconque, par exemple parce que son mandat a expiré. Dans ce cas, un type de contrat spécial a été prévu, c’est le AssociationAdministrationOwnerchange. C’est un contrat instancié par toute personne qui désire être promue en tant que président. Les membres de l’association votent alors pour cette dernière personne en appelant la fonction vote du contrat. Notez qu’il y a un peu d’héritage ici car j’utilise une notion abstraite de contrat d’administration. Cette classe abstraite sert aussi l’exclusion d’un membre, l’auto-destruction de l’association et la demande de référendum.

abstract contract AssociationAdministration {
  
   AssociationOrg public assoCtr;
   address payable public proposedMember;
   mapping (address => bool) public didVotes;
   uint public voteCount;
 
   /// Vote for the administration action
   function vote() public onlyMembers {
       if (!didVotes[msg.sender]) {
           didVotes[msg.sender] = true;
           voteCount ++;
       }
   }
   ...
}
contract AssociationAdministrationOwnerchange is AssociationAdministration {
   AdminAction public adminAction = AdminAction.OWNERCHANGE;
   constructor(address _assoCtr) {
       proposedMember = msg.sender;
       assoCtr = AssociationOrg(_assoCtr);
       require(msg.sender != assoCtr.owner(), "New owner cannot be old owner");
   }
   ...
}

ETAPE 4

Le prétendant au trône instancie un contrat de type AssociationAdministrationOwnerchange et récupère l’adresse de son contrat de changement de règne. Il communique alors aux membres de l’association concernée le contrat de changement et leur demande de voter pour lui. Ceux-ci appellent la fonction vote() de ce contrat. Le prétendant peut régulièrement vérifier le nombre de vote pour lui en récupérant la variable voteCount de son contrat. Lorsqu’il pense que c’est bon, il appelle la fonction handleOwnerchangeAction() du contrat d’association en passant en paramètre son contrat de changement de président. Si les conditions sont favorables (notamment 51% des votes), le contrat change et il devient owner. Notons que tous les votes sont publics par définition.

contract AssociationOrg {
   function handleOwnerchangeAction(address payable _adminCtr) public maintenanceOff {
       AssociationAdministrationOwnerchange assoAdminCtr = AssociationAdministrationOwnerchange(_adminCtr);
       require(assoAdminCtr.adminAction() == AssociationAdministration.AdminAction.OWNERCHANGE, "Ownerchange action required");
       require(assoAdminCtr.assoCtr() == this, "Invalid AdministrationContract reference to MoroccanContract");
       require(assoAdminCtr.voteCount() > membersCount/2, "Need at least 51% of the votes");
       require(assoAdminCtr.proposedMember() != owner, "New owner cannot be the current owner");
       require(members[assoAdminCtr.proposedMember()], "New owner is not a member");
       owner = assoAdminCtr.proposedMember();
   }
   ...

Comment interagir avec une blockchain Ethereum ?

Alors concrètement, comment faire pour réaliser toutes ces tâches ?

1- Cloner le repository git

git clone [email protected]:AshtonIzmev/ethereum-association.git

2- Installer Node.js et npm puis les packages suivants :

node -v && npm -v
npm install -g truffle && npm install -g ganache-cli

3- Lancer une blockchain de test. par défaut, elle crée 100 comptes de test avec 100 ether dessus chacun et affiche les clés privée associées pour les manipuler. Une autre possibilité est d’utiliser une version graphique de ganache plus user-friendly.

ganache-cli -p 8545 -i 1337

4- Lancer les tests unitaires sur les contrats pour être sur que tout fonctionne (le fichier truffle.js est utilisé)

./run_test.sh

Ils doivent tous êtres verts ou passing.

5- La blockchain tourne, le code est prêt et il faut le déployer sur la blockchain de test. On se place dans le dossier sol/truffle

truffle compile
truffle migrate
// la migration va exécuter les scripts dans le dossier migration
// notamment 2_association_org_contract.js qui déploie une association

La réponse devrait contenir l’adresse du contrat sur ce format :

2_deploy_contracts.js
=====================
 
  Deploying 'AssociationOrg'
  --------------------------
  > transaction hash:    0x4da04c2a27a6b4bb5f3f00e215d20b214bce24007cea9962c8dfef575b8e2561
  > Blocks: 0            Seconds: 0
  > contract address:    0x60A3deBab1861a0E4faDcCAdAaE1593f79912eBd
  > block number:        140
  > block timestamp:     1605398255
  > account:             0x1cEC69A791637F7fbBA61Ac0C5074eEa5Fb29BF5
  > balance:             99.250537
  > gas used:            1884549 (0x1cc185)
  > gas price:           20 gwei
  > value sent:          0 ETH
  > total cost:          0.03769098 ETH
 
 
  > Saving migration to chain.
  > Saving artifacts
  -------------------------------------
  > Total cost:          0.03769098 ETH
 
 
Summary
=======
> Total deployments:   1
> Final cost:          0.03769098 ETH
> contract address:    0x60A3deBab1861a0E4faDcCAdAaE1593f79912eBd

6- Nous pouvons maintenant interagir avec notre association en vérifiant par exemple que l’owner a bien été défini

truffle console
let instance = await AssociationOrg.deployed()
let ownerStr = await instance.owner();
// On verifie que l'owner est bien le compte utilisé (par défaut le premier de accounts)
ownerStr == accounts[0]

L’interaction pourrait continuer comme cela pour la cooptation et l’administration du contrat mais les tests unitaires déroulent des scénarios qui prouvent que le fonctionnement est correct. Il est temps de passer à une interface plus intuitive.

Une application grand public

Pour interagir de manière plus simple avec notre blockchain que d’appeler des fonctions dans une console truffle, nous allons créer une application web statique qui portera dans du code javascript la logique d’appel à la blockchain.

Cette application web se trouve dans le dossier web de mon repository github. Trois types de fichiers sont importants ici :

  1. Le fichier index.html qui contient simplement la page html qui sera affichée. Rien d’extraordinaire sinon une petite page bootstrap
  2. Le fichier curieux.js qui contient l’ensemble de la logique d’appel à la blockchain masquée par des boutons et une interface user-friendly
  3. Les différents contrats compilés en json utilisés par curieux.js pour instancier les contrats et appeler les fonctions

Pour appeler la blockchain, l’application web a besoin d’une wallet sécurisée. Ce rôle est joué par l’extension MetaMask. Le fichier javascript curieux.js fait appel à la librairie web3js qiu possède des fonctions natives de communication avec la wallet MetaMask.

On doit tout d’abord injecter la dernière version de la librairie web3js pour manipuler la wallet MetaMask et interagir avec la blockchain avec l’API javascript.

<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>

Pour récupérer le context MetaMask, cette fonction asynchrone va se révéler indispensable :

async function getWeb3() {
   // Wait for loading completion to avoid race conditions with web3 injection timing.
   if (window.ethereum) {
       const web3 = new Web3(window.ethereum);
       try {
           // Request account access if needed
           await window.ethereum.enable();
           // Acccounts now exposed
           return web3;
       } catch (error) {
           console.error(error);
       }
   } else if (window.web3) {
       // Use Mist/MetaMask's provider.
       const web3 = window.web3;
       console.log('Injected web3 detected.');
       return web3;
   } else {
       const provider = new Web3.providers.HttpProvider('http://' + ethereumHost + ':' + ethereumPort);
       const web3 = new Web3(provider);
       console.log('No web3 instance injected, using Local web3.');
       return web3;
   }
};

On récupère le solde de la wallet MetaMask (pour l’afficher sur la page) avec le code suivant

getWeb3().then((web3) => {
   web3.eth.getCoinbase(function (err, account) {
       if (err === null) {
           web3.eth.getBalance(account, function (err, balance) {
               $("#accountAddress").html("Votre adresse public / compte Ethereum: " + account + " avec un solde de " + web3.utils.fromWei(balance, 'ether') + "ETH");
           });
       }
   });
});

Pour instancier un contrat dans la blockchain :

getWeb3().then((web3) => {
   web3.eth.getCoinbase(function (err, account) {
       if (err !== null) {
           console.log(error);
           return;
       }
       $.getJSON('contracts/MonContrat.json', function (data) {
           let abi = data['abi'];
           let bytecode = data['bytecode'];
           let ctr = new web3.eth.Contract(abi);
           ctr.deploy({
               data: bytecode,
               arguments: args
           })
               .send({ from: account }, function (error, transactionHash) { console.log(transactionHash); })
           ...
       ...
   ...
}

Et enfin pour appeler des fonctions sur un contrat déjà instancié. Ici on récupère la variable publique name d’un contrat d’association pour l’afficher dans la balise name-seek-assoc

ctr.methods.name().call().then(function (name) {
   $("#name-seek-assoc").html(escapeXml(name));
}).catch(function (error) { showToast(); console.log(error); return; });

Ainsi, nous avons toutes les briques pour rendre la manipulation d’une blockchain une opération transparente pour l’utilisateur à travers cette Decentralized Application web. Lorsque ce dernier navigue sur le site curieux.ma, le chargement de la librairie connecte automatiquement sa wallet avec le code javascript du site et le tour est joué.

Afin de respecter au maximum l’esprit de la blockchain, cette Dapp est totalement statique et ne dispose d’aucun back-office ni base de données. Cet esprit est celui d’être un intermédiaire parfaitement auditable (c’est le code javascript) et donc de confiance pour qui décide de cliquer sur un bouton sans avoir les compétences techniques pour disséquer le code.

Conclusion

Le développement d’une application dans la blockchain Ethereum n’a rien de sorcier. Il suit une logique certe propre mais cohérente avec le monde de la programmation objet et plus généralement du software engineering avec ses tests et bonnes pratiques. L’écosystème d’outils est tellement riche qu’il est possible d’abstraire la complexité technique de nos manipulations et d’en faire un vrai usage porté sur l’internaute non initié.

J’espère que cet article vous aura donné envie de vous essayer également aux smart contracts et d’en faire de sympathiques applications pour contribuer à la mise en œuvre de ce beau concept qu’est la blockchain.

Liens vers les photos

Photo by Lopez Robin on Unsplash