Projet de Vote sur la Blockchain Ethereum (2/2) : Fin de l'écriture du Smart Contract

  • posté le
  • par ESENS

Dans la première partie de cet article - disponible par ici -, nous avons vu comment créer un scrutin. Nous allons voir à présent comment ajouter une proposition à ce scrutin et comment soumettre un vote.

Ajout d’une proposition à un scrutin

Pour ce deuxième test nous allons introduire la notion de proposition d’un scrutin. Un scrutin peut contenir plusieurs propositions pour lesquelles les utilisateurs pourront voter.

Retournons donc sur la suite de tests et rajoutons un test couvrant cette fonctionnalité.

Relançons les tests :

truffle test
Contract: Vote
   ✓ should emit a ScrutinCreated event when createScrutin (60ms)
   1) should emit a ProposalCreated event when createProposal is called on an existing Scrutin
   Events emitted during test:
   ---------------------------
   ScrutinCreated(_scrutinId: 0, _name: Best fast food ?, _scrutinOwner: 0x627306090abab3a6e1400e9345bc60c78a8bef57)
   ---------------------------
 1 passing (286ms)
 1 failing
 1) Contract: Vote
      should emit a ProposalCreated event when createProposal is called on an existing Scrutin:
    Error: VM Exception while processing transaction: revert

Évidemment, le second test a échoué. Allons implémenter la fonctionnalité sur le Smart Contract :

Relançons les tests :

truffle test
  Contract: Vote    
    ✓ should emit a ScrutinCreated event when createScrutin (58ms)    
    ✓ should emit a ProposalCreated event when createProposal is called on an existing Scrutin (88ms)
 2 passing (303ms)

Félicitations, notre deuxième fonctionnalité est implémentée.

Pour cela nous avons défini une nouvelle 'struct' et une liste pour la stocker :

struct Proposition {   
  uint scrutinId;   
  string description;   
  uint counter;
}
Proposition[] propositions;

Et dans la méthode 'createProposal()' nous vérifions que le scrutin appartient bien à la même personne qui soumet la proposition puis nous initialisons la proposition et lançons un événement de création de proposition.

function createProposal(uint _scrutinId, string _description) public {   
  Scrutin storage scrutin = scrutins[_scrutinId];   
  require(scrutin.scrutinOwner == msg.sender);   
  uint _propositionId = propositions.push(Proposition(_scrutinId, _description, 0)) - 1;   
  emit ProposalCreated(_propositionId, _scrutinId, _description);
}

Afin de s’assurer de la partie vérification nous pouvons rajouter un test sur l’ajout d’une proposition sur un scrutin que nous n’avons pas créé :

Avec pour résultat :

truffle test
  Contract: Vote    
    ✓ should emit a ScrutinCreated event when createScrutin (60ms)    
    ✓ should emit a ProposalCreated event when createProposal is called on an existing Scrutin (92ms)    
    ✓ should fail if proposal is submitted on a scrutin that we don't own (49ms)
  3 passing (435ms)

Soumettre un vote

Jusqu’à maintenant, nous pouvons créer un scrutin et ajouter des propositions à celui-ci.

La suite logique serait de pouvoir voter pour une proposition.

Commençons par écrire un petit test d’un cas passant :

Je vous passe l'exécution du test, mais évidemment il est en échec.

Passons à l’implémentation côté Smart Contrat :

Maintenons lançons le test :

truffle test
Contract: Vote    
  ✓ should emit a ScrutinCreated event when createScrutin (55ms)    
  ✓ should emit a ProposalCreated event when createProposal is called on an existing Scrutin (89ms)    
  ✓ should fail if proposal is submitted on a scrutin that we don't own (45ms)    
  ✓ should emit a VoteSubmitted event if proposal exist (96ms)
 4 passing (584ms)

Bravo ! Nous pouvons maintenant voter !

Cependant on aimerait peut être limiter la participation à un seul vote par scrutin par utilisateur.

Ajoutons donc un test pour cette vérification :

En exécutant le test,nous aurons comme message :

Contract: Vote    
  ✓ should emit a ScrutinCreated event when createScrutin (56ms)    
  ✓ should emit a ProposalCreated event when createProposal is called on an existing Scrutin (92ms)    
  ✓ should fail if proposal is submitted on a scrutin that we don't own (45ms)    
  ✓ should emit a VoteSubmitted event if proposal exist (110ms)    
  ✓ should fail if user has already voted on a scrutin (91ms)    
1) "after each" hook 5 passing (765ms)  1 failing
  1) Contract: Vote       "after each" hook:     Uncaught AssertionError: Did not fail

L’appel n’a pas échoué, modifions maintenant la méthode pour prendre en compte ce cas :

Le résultat des tests :

Contract: Vote    
  ✓ should emit a ScrutinCreated event when createScrutin (58ms)    
  ✓ should emit a ProposalCreated event when createProposal is called on an existing Scrutin (94ms)    
  ✓ should fail if proposal is submitted on a scrutin that we don't own (44ms)    
  ✓ should emit a VoteSubmitted event if proposal exist (98ms)    
  ✓ should fail if user has already voted on a scrutin (94ms)
 5 passing (754ms)

Félicitations ! Nous ne pouvons voter qu’une seule fois par scrutin. Pour faire cette vérification, j’ai utilisé un 'mapping'. C’est une structure de donnée semblable à une map qui va me donner pour une entrée donnée, le résultat associé.

Ici, j’ai enchaîné deux mapping de la façon suivante :

mapping(address => mapping(uint => bool)) private isScrutinVoted;

Le mapping 'isScrutinVoted' va tout d’abord prendre en entrée l’adresse d’un utilisateur pour nous donner un deuxième mapping qui prendra en entrée l’identifiant d’un scrutin pour enfin nous retourner 'vrai' ou 'faux' à la question 'est-ce que cette utilisateur a déjà participer à ce scrutin ?'.

La notion de mapping est très importante car, comme dit plus haut, chaque transaction coûte du gaz. Si nous avions codé une boucle for pour vérifier si l’utilisateur avait déjà participé au scrutin, nous aurions dû maintenir une liste de vote et itérer sur chaque élément. Notre algorithme aurait donc eu une complexité de O(n). Cela veut dire que dans le pire des cas, nous aurions parcouru tous les éléments de la liste si celui qui nous intéressait était en dernier. Chaque opération inutile aurait donc été facturée directement à l’utilisateur.

Notre but étant de réduire au maximum les coûts d'utilisation de notre Ðapp, j’utilise donc un mapping qui permet de retrouver l’élément que nous cherchons avec une complexité de 0(1). L’élément est toujours retrouvé du premier coup, nous ne gâchons donc pas de puissance de calcul chez les mineurs.

Revenons en à la méthode 'submitVote()'. Au premier appel, la vérification passera en succès car la valeur du mapping est à 'false', c’est la valeur par défaut.

require(isScrutinVoted[msg.sender][proposition.scrutinId] == false);


Et une fois que le compteur de vote de la proposition est incrémenté, nous passons la valeur du mapping à 'vrai', l’utilisateur a déjà participé au scrutin :

isScrutinVoted[msg.sender][proposition.scrutinId] = true

On ne peut donc pas voter plusieurs fois dans un même scrutin.

Conclusion

Notre Smart Contract est implémenté, testé et prêt à être déployé. Nous avons donc 99 lignes de tests pour 55 lignes pour le Smart Contract.

Ces tests ont permis de s’assurer que les interactions avec le Smart Contract soient bien implémentées comme défini et éviter quelques bugs qui auraient pu se glisser sans tests.

Donc s’il vous plaît, n'oubliez pas de tester votre code avant la mise à disposition à vos utilisateurs !

Le code est sûrement améliorable, n’hésitez pas à me faire des commentaires sur le GitHub du projet.

Pour continuer à jouer avec le contrat, je vous propose quelques pistes pour l’enrichir :

  • ajouter une date de début d’un scrutin
  • ajouter une date de clôture d’un scrutin
  • proposer des scrutins ouvert aux propositions d’autres utilisateurs

Dans un prochain article nous développerons la partie front-end web en JavaScript pour interagir avec le Smart Contract.

Le code complet est disponible ici : https://github.com/esensconsulting/tdd-vote-on-ethereum

Article rédigé par Anthony M. | Retrouvez tous nos articles sur le Blog ESENS


Vous êtes à la recherche d'un nouveau challenge ? Rejoignez l'équipe ESENS en postulant à nos offres d'emploi !

PARTAGER CET ARTICLE