Home R&D Webnet Implémenter sa propre blockchain privée : Interactions avec une application PHP Symfony via Web3

Implémenter sa propre blockchain privée : Interactions avec une application PHP Symfony via Web3

  André, Architecte technique PHP 5 min 11 mars 2019

Maintenant que nous avons vu comment créer notre propre blockchain privée et comment nous utilisons les smart contracts, nous allons voir comment faire pour que notre application de pronostics sportifs puisse interagir avec la blockchain via une API.

De quoi est composée notre API ?

Notre API a été développée en PHP en se basant sur le framework Symfony 4 et sur la bibliothèque « web3 » (https://github.com/sc0Vu/web3.php).

La bibliothèque web3 est une interface pour interagir avec la blockchain et l’écosystème Ethereum (https://web3js.readthedocs.io/en/1.0/index.html)

Elle va nous permettre d’accéder aux données des smart-contrats, d’écrire de nouveaux blocs, de manipuler les utilisateurs de la blockchain (création et consultation des balances).

Dans notre cas d’usage nous avons également eu besoin d’une base de données relationnelle, et nous avons utilisé le moteur MariaDB. Nous reviendrons plus en détails quant à l’utilisation de cette base de données dans un second temps.

Comptes utilisateurs

Toute opération sur la blockchain, quelle qu’elle soit, requiert une authentification. Pour cela, il faut à minima un compte sur la blockchain.

Comme nous avons vu dans le post « Créer sa blockchain privée », un compte super utilisateur a été créé pour notre cas d’usage, et c’est avec ce compte que seront effectuées l’ensemble des transactions au sein de la blockchain.

Un compte utilisateur sur la blockchain est simplement composé d’un login (qui se traduit par une adresse de hash) et d’un mot de passe (définit lors de la création de compte).

Dans notre cas, un collaborateur Webnet se connectera sur l’application de pronostic en ligne avec ses identifiants Office365 (adresse email) et pour des raisons d’ergonomie et de simplicité d’utilisation, nous avons fait en sorte que son authentification au sein de la blockchain se fasse de manière transparente. Nous n’avons ainsi pas souhaité qu’il s’authentifie avant chaque placement ou modification de pronostic.

Pour bénéficier de ce confort, nous avons été dans l’obligation de sauvegarder quelque part cette association email/hash, qui est essentielle au bon fonctionnement de l’application, et nous avons opté pour la base de données MariaDB.

Initialisation des comptes utilisateurs sur la blockchain

Lorsqu’un collaborateur de Webnet utilise l’application de pronostic en ligne, avant chacune de ses interactions avec la blockchain, nous effectuons une vérification pour savoir s’il possède ou non un compte sur la blockchain.

  • Si celui-ci n’en possède pas encore, nous lui en créons automatiquement un, mais sans lui laisser la possibilité de personnaliser son mot de passe.

En effet nous imposons cette valeur qui variera pour chaque collaborateur étant donné qu’il s’agira simplement de son adresse email pour une question de simplicité.

Le mot de passe est différent pour chaque utilisateur  pour des raisons que nous aborderons un peu plus tard.

Exemple de création de compte sur la blockchain

Le hash devient le login de l’utilisateur sur la blockchain.

Nous avons donc une association à faire entre l’identifiant d’un utilisateur au sein de l’application et l’identifiant lui correspondant au sein de la blockchain.

Exemple de comptes utilisateurs stockés en base de données:

Question: Vous allez nous dire oui mais le fait d’avoir une base de données n’est-il pas synonyme de faille ?

Prenons un exemple concret : Si quelqu’un parvient à accéder à la base de données et supprime le hash associé à un email ? ou le modifie ?

  • Si un hash est modifié, alors la connexion sur la blockchain ne sera plus possible. Le super utilisateur ne pourra plus placer de pronostic pour cet utilisateur
  • Si un hash est supprimé, alors au prochain pronostic placé, un nouveau compte sera créé pour cette personne.

Identification d’un utilisateur

La librairie Web3 nous permet de facilement s’authentifier au sein de la blockchain. Voici un exemple d’utilisation :

/**
 * Permet de s'assurer que le login/mdp d'un utilisateur est correct
 *
 * @param string $accountAddress L'adresse du compte utilisateur au sein de la blockchain
 * @param string $password       Le mot de passe de l'utilisateur *
 * @throws \Exception
 */
public function checkAccountRights(string $accountAddress, string $password)
{    
    $web3 = new Web3('http://localhost:8545');
    $web3->getPersonal()->unlockAccount(
        $accountAddress,
        $password,
        function ($err, $unlocked) {
            if ($err !== null || !$unlocked) {
                throw new \Exception("/!\ USURPATION D'IDENTITE /!\ ", 400);
            }
        }
    );
}

Ici, si le couple login (hash) / mode de passe est correct, alors le compte utilisateur est déverrouillé pour un certain temps (définissable), dans le cas contraire une exception sera levée.

Maintenant prenons un autre exemple : Si quelqu’un venait à intervertir le hash d’un utilisateur « riche » et celui d’un utilisateur « moins riche » directement en base de données ?

  • Effectivement c’est plausible, les conséquences d’une telle action seront que les comptes concernés ne pourront plus faire de pronostic car l’authentification des utilisateurs sera en échec du fait que le couple login (hash)/mot de passe ne correspondra pas.

Quelque soient les changements effectués sur la base de données, cela n’aura aucun impact sur les données de la blockchain. Nous aurons toujours la possibilité de retrouver les comptes utilisateurs pour chaque personne.

Vu que nous avons la possibilité de lister l’ensemble des comptes utilisateurs de la blockchain :

/**
 * Récupère la liste des comptes présents sur la blockchain
 *
 * @return array
 */
public function getAccounts()
{
    $return = [];
    $web3 = new Web3('http://localhost:8545');
    $eth = $web3->eth;

    $eth->accounts(function ($err, $accounts) use ($eth, &$return) {
        if ($err) {
            throw $err;
        }

        $return = $accounts;
    });

    return $return;
}

Nous avons toujours la possibilité de faire du brute force en bouclant sur l’ensemble des comptes afin de vérifier les mots de passe de chacun (puisqu’il s’agit de l’email de chacun)

Nous pourrions ainsi réassocier les hashs pour chacun des utilisateurs. Je vous l’admets ce n’est pas très sexy mais cette solution est tout à fait envisageable si les données de la base de données venaient à être altérées.

Maintenant que nous savons comment créer des comptes au sein de la blockchain et comment authentifier un utilisateur, nous allons voir comment nous avons procédé pour déclarer un contrat (qui correspond à un nouveau match) et comment interagir avec un contrat (qui correspond aux pronostics des utilisateurs sur un match).

Mais avant tout, regardons de plus près les smart-contrats.

Smart-Contrats

Nous avons vu dans le post « Rédaction de smart-contracts avec Solidity et Truffle » comment compiler le contrat une fois que celui-ci avait été finalisé. Nous allons voir maintenant comment utiliser le contrat compilé : « Game.json », au sein de notre application Symfony.

Ce fichier JSON compilé contient :

  • L’ABI : l’interface binaire d’application, qui décrit les méthodes permettant d’interagir avec le contrat (entrées/sortie). C’est un élément fondamental dont nous aurons besoin à chaque appel de méthode sur un contrat (création de match, pronostic etc.).
  • le « byteCode » qui est le résultat d’une compilation que la machine virtuelle Ethereum peut comprendre et servira lors de la création des contrats.

Exemple pour récupérer l’ABI sur le smart-contrat :

/**
 * Permet de récuperer le contrat
 *
 * @param string $contractName Nom du contrat
 *
 * @return mixed
 */
private function loadContractFile(string $contractName)
{
    $filename = "repertoire_ou_se_trouve_le_contrat/$contractName.json";

    return json_decode(file_get_contents($filename), true);
}

/**
 * Permet de récupérer l'ABI du contrat
 *
 * @param string $contractName Nom du contrat
 *
 * @return string
 */
public function getContractAbi(string $contractName)
{
    return json_encode($this->loadContractFile($contractName)['abi']);
}

 Exemple pour récupérer le byteCode sur le smart-contrat :

/**
 * Permet de récupérer le byteCode d'un contrat
 *
 * @param string $contractName Nom du contrat
 *
 * @return mixed
 */
public function getContractByteCode(string $contractName)
{
    return $this->loadContractFile($contractName)['bytecode'];
}

Maintenant que nous avons tous les éléments pour interagir avec la blockchain, nous allons voir comment créer un contrat.

Création d’un match

Chaque match correspond à un contrat différent et chaque pronostic sur un match se traduit par une nouvelle transaction sur le contrat concerné.

La création de contrat ne peut se faire instantanément comme la création d’un utilisateur puisqu’elle requiert du minage.

Nous devons donc faire un travail en amont dès lors qu’un match est prévu, nous allons sauvegarder en base de données les informations du match : à savoir la date, les équipes, le type de compétition, etc… Il faut à minima tous les éléments nécessaires au bon déroulement du pronostic.

Ces éléments seront repris lors de la création du contrat.

Note importante : tous les éléments du contrat ne sont pas toujours éditables. Par exemple on pourra modifier le statut d’un match (à venir, en cours, terminé) mais on ne pourra pas changer les équipes participantes. Pour ce faire il faudra annuler le match et en recréer-un.

L’édition d’une propriété du contrat dépendra du type de celle-ci au sein du contrat.

Nous avons vu ci-dessus qu’une authentification était nécessaire avant toute opération sur la blockchain, quelle qu’elle soit. La création d’un nouveau contrat n’échappe pas à la règle et c’est notre super utilisateur que nous utiliserons pour effectuer cette opération.

Exemple de création de contrat :

$abi = $contractService->getContractAbi($contractName);
$byteCode = $contractService->getContractByteCode($contractName);
$contract = new Contract('http://localhost:8545', $abi);

$contract->bytecode($byteCode)->new(
    $matchData['id'],
    $matchData['team1Id'],
    $matchData['team2Id'],
    $matchData['gameType'],
    [
        'from' => '0x1220be9f5201c7c00f3c13cc773e8696962b810a',
        'gas' => '0x400d40',
    ],
    function ($err, $transactionId) {
        if ($err) {
            throw $err;
        }

        // Sauvegarde en BDD de l'identifiant de transaction: $transactionId
    }
);

Analyse de code:

La définition des méthodes getContractAbi et getContractByteCode correspond à celles que nous avons vues un peu plus tôt. Avec ces méthodes nous avons identifié sur quel contrat nous voulons interagir.

Ensuite la nouvelle instance de Contract requiert l’url de la blockchain concernée et l’ABI du smart-contrat.

Pour finir nous appelons la méthode « new » de notre contrat.
Cette méthode attend 4 arguments obligatoires qui sont :

  • L’identifiant du match
  • L’identifiant de l’équipe 1
  • L’identifiant de l’équipe 2
  • Le type de match (match de poule ou éliminatoire, la récompense dépendra du type)

Mais lors de l’appel nous spécifions un 5ème argument qui est un tableau permettant de spécifier quel est l’utilisateur originaire de l’opération.

Pour rappel toute opération au sein de la blockchain engendre un coût et ici c’est notre super utilisateur qui prendra à sa charge (et donc à ses frais) la création d’un nouveau contrat.

Le « gas » spécifié ici correspond en quelque sorte au montant que possède dans son porte-monnaie notre super utilisateur, c’est avec cette quantité de « gas » qu’il demande à effectuer l’opération. Si cette somme est insuffisante, alors aucune action ne sera effectuée sur la blockchain.

En retour de cette méthode « new », nous aurons un identifiant de transaction qui est à sauvegarder en base de données. Cet identifiant nous sera utile pour savoir à quel moment les utilisateurs pourront pronostiquer sur ce nouveau match.

En effet, la création d’un match requiert du minage pour que ce contrat soit initialisé. Dès lors que celui-ci sera initialisé, nous pourrons alors récupérer son adresse de contrat.

Voilà un autre élément important pour la suite. Cette adresse est en quelque sorte l’identifiant unique du contrat dans la blockchain (donc du match) et nous sera utile pour enregistrer les pronostics sur ce match.

Afin de récupérer cette adresse de contrat, nous avons mis en place une routine qui va aller vérifier le statut des matchs concernés (à partir des identifiants de transactions que l’on a stocké en base de données).

Dès lors que nous récupérons une adresse de contrat, nous la stockons en base de données et nous changeons le statut du match : désormais les collaborateurs de Webnet peuvent pronostiquer sur ce match.

Pronostic d’un collaborateur

Pre-requis :

  • Le match sur lequel le collaborateur veut parier doit avoir été initialisé sur la blockchain (créé et miné)
  • L’adresse du contrat sur le match en question est connue.

C’est depuis l’application finale que les collaborateurs pourront saisir de nouveaux pronostics ou bien en éditer (tant que c’est possible bien évidemment, nous n’allons pas autoriser un collaborateur à éditer son pronostic une fois le match commencé, voir terminé).

L’application est interfacée à notre API qui interagira directement avec la blockchain.

Comme nous l’avons déjà vu, avant toute opération sur la blockchain, il faut préalablement s’authentifier. Ici c’est le compte du pronostiqueur qui s’authentifie. Pour rappel, l’identifiant de celui-ci est stocké en BDD et son mot de passe est son email.

Scénario possible :

  • Le collaborateur n’a pas encore de compte utilisateur sur la blockchain : nous lui en créons un à la volée
  • L’authentification échoue : les données en BDD ont probablement été altérées.
  • L’authentification est en succès, nous pouvons placer le pronostic, c’est-à-dire effectuer une transaction au sein du contrat.

Avant de faire une nouvelle opération sur la blockchain, nous sauvegardons en base de données le pronostic que le collaborateur à fait.

Exemple d’un nouveau pronostic :

$abi = $contractService->getContractAbi($contractName);
$contract = new Contract('http://localhost:8545', $abi);

$contract->at($contractAddress)->send(
    'PlaceBet',
    $betData['matchId'],
    $betData['better'],
    $betData['bettedHomeScore'],
    $betData['bettedAwayScore'],
    $betData['bettedWinner'],
    [
        'from' => '0x1220be9f5201c7c00f3c13cc773e8696962b810a',
        'gas' => '0x400d40',
    ],
    function ($err, $transactionId) {
        if ($err) {
            throw $err;
        }
                // ...
    }
);

Analyse du code

La définition de la méthode getContractAbi correspond à celle que nous avons vu un peu plus tôt.

La variable $contractAddress correspond à l’adresse du contrat pour le match en question (l’identifiant unique que nous avons récupéré avec la routine)

Nous utilisons la méthode send() du contrat pour appeler une méthode particulière.
Le nom de la méthode à appeler se situe en premier argument de méthode.
Les arguments à venir correspondent aux arguments de la méthode en question.
Ici la méthode ‘PlaceBet’ attend :

  • L’identifiant de match sur lequel le collaborateur veut placer son pronostic (permet de s’assurer la cohérence des données de la transaction)
  • L’identifiant sur la blockchain du pronostiqueur
  • Le score pronostiqué pour l’équipe 1
  • Le score pronostiqué pour l’équipe 2
  • Le numéro de l’équipe vainqueur (1 ou 2)

Et en dernier argument de méthode, nous retrouvons un tableau contenant les informations de notre super utilisateur et du « gas » alloué pour cette opération : Placer un pari.

  • L’action sur la blockchain est effectuée pour notre compte super utilisateur au nom du collaborateur. Chaque opération coûtant de l’ether, nous avons fait ce choix pour le coût des transactions soit transparent vis-à-vis des collaborateurs.

Le retour de cette méthode send() contiendra également un identifiant de transaction qui pourra servir dans le futur pour consulter l’état de cette opération.

Nous avons vu que nous avions besoin d’une base de données pour sauvegarder plusieurs choses :

  • Les identifiants d’utilisateur
  • Les informations sur les matchs
  • Les pronostics des collaborateurs

La question « est-ce réellement nécessaire de sauvegarder les informations sur les matchs ? » a le mérite d’être pertinente car la sécurisation des données reste primordiale.

  • A chaque fois que nous interrogeons la blockchain cela à un coût, pour limiter ces appels et surtout pour gagner en fluidité au sein de l’application, nous allons utiliser les données stockées en base de données comme un cache d’application.
  • Il faut bien garder à l’esprit que c’est un « cache », nous avons donc prévu une méthode pour rafraîchir l’ensemble des données depuis la blockchain afin de mettre à jour ce « cache » et ainsi contourner le problème lié à la sécurisation des données. (Si quelqu’un était amené à modifier les valeurs en BDD)

Attribution de récompenses

Une fois qu’un match est terminé, il faut maintenant procéder à l’ultime étape qui est de calculer les récompenses. Pour chaque pronostic, un traitement permet de définir à hauteur de combien s’élève la récompense.

Dans notre cas d’usage nous attribuons des points si le pronostiqueur à misé sur le vainqueur du match, mais se verra également attribuer des points supplémentaires s’il a misé sur le score exact, etc…

Le transfert d’ether s’effectue entre le compte de notre super utilisateur et celui du collaborateur.

Nous avons également une sécurité au sein du contrat qui bride à une seule exécution le calcul des récompenses pour qu’il n’y ait pas plusieurs paiements sur un même match.

Exemple de d’attribution de récompense :

$abi = $contractService->getContractAbi($contractName); 
$contract = new Contract('http://localhost:8545', $abi);

$contract->at($contractAddress)->send(
    'RewardAllBets',
    $matchId,
    [
        'from' => '0x1220be9f5201c7c00f3c13cc773e8696962b810a',
        'gas' => '0x400d40',
        'value' => '0x' . Utils::toWei(($bettersCount * 3), 'ether')->toHex()
    ],
    function ($err, $result) use ($match, $self) {
        if ($err) {
            throw $err;
        }

        // Mise à jour du statut du match : ‘rewarded’ en BDD pour des problématiques de cache 
    }
);

Analyse de code:

Nous utilisons de nouveau la méthode send() pour interagir avec la méthode ‘RewardAllBets » de notre contrat.
Ici notre méthode n’attend qu’un seul argument qui est l’identifiant du match.

Pour ce qui est du dernier argument de la méthode, vous avez peut-être aperçu qu’une nouvelle ligne était présente. Il s’agit là du montant maximum des transactions dans l’éventualité que tous les pronostiqueurs aient vu juste.

Ce qui correspond au calcul suivant : Nombre de pronostiqueurs * gains maximum (3 ethers)

Conclusion

Nous avons vu en détails l’ensemble des étapes entre le moment où l’on initialise un match et l’attribution des récompenses aux différents pronostiqueurs.

Comme on peut le constater on peut, en effet, adapter un workflow d’une application existante à la blockchain. Mais cela se fait au prix de quelques circonvolutions et une grande augmentation de la complexité de l’application.

Notre approche n’est surement pas la plus optimale, mais cela nous a permis de conserver le même workflow et surtout nous avons maintenant une bien meilleure idée du fonctionnement interne de la blockchain et des smart-contracts.

Notre projet désormais terminé, il est l’heure de faire un point sur notre ressenti sur cette implémentation d’une blockchain privée au sein d’un projet Symfony et plus généralement sur l’intérêt de cette technologie sur des problématiques non financières.

Lire les articles similaires

Laisser un commentaire

Social Share Buttons and Icons powered by Ultimatelysocial