Blockchain locale
En développement et en CI, les contrats tournent sur un nœud Hardhat local, avec un coordinateur Chainlink VRF simulé et un petit oracle qui répond aux demandes de tirage. Le backend s'y connecte sans aucune configuration : les adresses des contrats sont déterministes et correspondent à ses valeurs par défaut. Cette page décrit le nœud, le script de déploiement, l'oracle, l'image Docker et les variables. Le démarrage de toute l'infrastructure locale est décrit dans Infrastructure locale.
Vue d'ensemble
flowchart LR
subgraph conteneur["Conteneur blockchain"]
node["hardhat node, port 8545"]
deploy["deploy-local.ts"]
oracle["local-vrf-responder.ts, compte 9"]
end
deploy -->|"déploie mock VRF, BlockLucky, POAP"| node
oracle -->|"lit RandomWordsRequested, appelle fulfillRandomWords"| node
backend["Backend, compte opérateur"] -->|"RPC_URL"| nodeDémarrer
Avec Docker
Le service blockchain de docker-compose.yml construit l'image de smart-contract/Dockerfile et publie le port sur 127.0.0.1:8545 uniquement.
docker compose up -d --wait blockchain--wait attend que le conteneur soit sain, c'est-à-dire que les contrats soient déployés.
Sans Docker
Dans un premier terminal, depuis smart-contract/ :
npm run nodeDans un second terminal, une fois le nœud prêt :
npm run deploy:local && npx hardhat run scripts/local-vrf-responder.ts --network localhostL'oracle tourne au premier plan tant que le terminal reste ouvert. Sans lui, les tours restent en Drawing après la demande de tirage.
Le nœud Hardhat
hardhat node lance un réseau Ethereum en mémoire, au chain ID 31337, avec 20 comptes dérivés de la phrase mnémonique publique de développement de Hardhat, chacun approvisionné en ethers fictifs. Un bloc est produit à chaque transaction.
| Index | Adresse | Rôle dans BlockLucky |
|---|---|---|
| 0 | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 |
Déployeur ; opérateur par défaut du backend, qui garde alors OPERATOR_ROLE, PAUSER_ROLE, MINTER_ROLE et DEFAULT_ADMIN_ROLE |
| 1 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 |
Trésorerie (LOCAL_TREASURY_ACCOUNT_INDEX) |
| 2 | 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC |
Administrateur de la Mairie (DEFAULT_ADMIN_ROLE sur les deux contrats) et propriétaire ConfirmedOwner (LOCAL_ADMIN_ACCOUNT_INDEX) |
| 9 | 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 |
Signataire de l'oracle VRF local |
La clé privée du compte 0 est la valeur par défaut de RELAYER_PRIVATE_KEY dans backend/src/config/env.config.ts. Ces clés sont publiques : elles ne doivent jamais recevoir de vrais fonds ni servir hors de la chaîne locale.
Attention
Le nœud ne conserve aucun état. À chaque redémarrage du conteneur, la chaîne repart du bloc 0 et les contrats sont redéployés aux mêmes adresses, mais vides. La base du backend, elle, garde les tours et tickets de la chaîne précédente, et l'indexeur détecte un curseur en avance sur la tête de chaîne. Il faut alors réinitialiser aussi la base : docker compose down -v, puis relancer les migrations et le seed.
Le script deploy-local.ts
scripts/deploy-local.ts enchaîne, avec le compte 0 :
| Nonce | Transaction | Résultat |
|---|---|---|
| 0 | Déploiement de VRFCoordinatorV2_5Mock(0.1 ether, 1 gwei, 4e15) |
0x5FbDB2315678afecb367f032d93F642f64180aa3 |
| 1 | createSubscription() |
Identifiant lu dans l'événement SubscriptionCreated |
| 2 | fundSubscription(subId, 10000 ether) |
Abonnement financé |
| 3 | Déploiement de BlockLucky |
0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 |
| 4 | addConsumer(subId, lottery) |
BlockLucky autorisé à demander de l'aléa |
| 5 | Déploiement de BlockLuckyPOAP |
0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 |
| 6 et suivants | Rôles, propriété VRF et approvisionnement de l'opérateur | Voir plus bas |
Pourquoi les adresses sont déterministes
L'adresse d'un contrat créé par un compte ne dépend que de l'adresse de ce compte et de son nonce. Le nœud démarre toujours avec les mêmes comptes, et le script envoie toujours les mêmes transactions dans le même ordre : le coordinateur naît au nonce 0, BlockLucky au nonce 3 et BlockLuckyPOAP au nonce 5. Ces adresses sont les valeurs par défaut de LOTTERY_CONTRACT_ADDRESS et POAP_CONTRACT_ADDRESS dans le backend et dans docker-compose.yml.
Conséquence : toute transaction ajoutée avant le nonce 5 dans le script, ou toute transaction envoyée par le compte 0 avant le déploiement, décale les adresses et casse la configuration par défaut du backend. L'identifiant d'abonnement, en revanche, change à chaque démarrage, car le mock le dérive notamment du hash du bloc précédent.
Configuration déployée
Le tour est configuré par roundConfigFromEnv avec une durée par défaut de 30 minutes. La configuration VRF est fixe :
| Champ | Valeur locale |
|---|---|
keyHash |
0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae |
callbackGasLimit |
500 000 |
requestConfirmations |
1 |
nativePayment |
false |
drawTimeout |
600 secondes |
Répartition des rôles
Les deux contrats sont construits avec le compte 0 comme administrateur provisoire. Le script calcule ensuite un plan de rôles avec planRoles de scripts/lib/roles.ts, le même module que pour Sepolia, l'applique avec applyRoles, puis le contrôle avec roleProblems : il s'arrête sur « Rôles incohérents » si un titulaire attendu manque. Il affiche enfin le tableau des titulaires (roleSummaryRows).
| Rôle | Titulaire en local |
|---|---|
DEFAULT_ADMIN_ROLE sur les deux contrats |
Compte 2 ; l'opérateur le reçoit aussi, en administrateur additionnel, sauf si LOCAL_STRICT_ROLES=true |
OPERATOR_ROLE et MINTER_ROLE |
Opérateur : OPERATOR_ADDRESS, sinon l'adresse de OPERATOR_PRIVATE_KEY, sinon le compte 0 |
PAUSER_ROLE |
PAUSER_ADDRESS, sinon l'opérateur |
Trésorerie (setTreasury) |
Compte 1 |
Propriétaire ConfirmedOwner |
Compte 2 : transferOwnership par le compte 0, puis acceptOwnership signé par le compte 2 dans le même script |
L'administrateur additionnel existe pour le reversement de commission de l'espace Mairie : le backend y appelle setTreasury, réservé à DEFAULT_ADMIN_ROLE, quand l'adresse saisie diffère de la trésorerie enregistrée. Le script le rappelle à la fin de son exécution.
Le compte 0 perd OPERATOR_ROLE, MINTER_ROLE et PAUSER_ROLE s'il n'est pas l'opérateur ou le pauseur, et renonce à DEFAULT_ADMIN_ROLE s'il n'est ni l'administrateur ni un administrateur additionnel. Deux cas en découlent :
| Clé opérateur | État final du compte 0 |
|---|---|
Aucune, ou la clé de développement par défaut (développement avec npm run dev) |
Opérateur, pauseur, frappeur et administrateur additionnel ; seule la propriété VRF passe au compte 2 |
Une autre clé (profil app de Compose) |
Aucun rôle ; l'opérateur reçoit en plus OPERATOR_FUNDING_ETH ethers (1 000 par défaut), car il paie le gaz du keeper, des frappes de badges et les dotations de bienvenue |
Dans docker-compose.yml, OPERATOR_PRIVATE_KEY reçoit RELAYER_PRIVATE_KEY.
Fichier de déploiement
Le script écrit smart-contract/deployments/localhost.json (ou dans DEPLOYMENTS_DIR), ignoré par Git. Il contient le réseau, le chain ID, la date, le bloc de déploiement, l'administrateur, l'opérateur, la trésorerie, les trois adresses, la configuration VRF (key hash, identifiant d'abonnement, nativePayment, propriétaire de l'abonnement, consommateur ajouté), le plan de rôles appliqué (roles), les arguments des deux constructeurs (constructorArgs) et la configuration des tours. L'oracle local le relit au démarrage.
L'oracle local
scripts/local-vrf-responder.ts joue le rôle du réseau Chainlink :
- il lit
deployments/localhost.json; - toutes les
VRF_POLL_INTERVAL_MSmillisecondes (2 000 par défaut), il récupère les événementsRandomWordsRequesteddu mock depuis le dernier bloc lu ; - pour chaque requête qu'il n'a pas encore servie, il appelle
fulfillRandomWords(requestId, consumer)sur le mock et attend la confirmation ; - il journalise chaque réponse et continue en cas d'erreur.
Il signe avec le compte d'index VRF_ORACLE_ACCOUNT_INDEX, 9 par défaut. Le compte est distinct de l'opérateur pour que deux processus n'envoient jamais de transactions avec le même compte : leurs nonces entreraient en conflit. Si l'index demandé n'existe pas, il se rabat sur le compte 0.
L'aléa fourni par le mock est keccak256(abi.encode(requestId, 0)) : déterministe, prévisible, et donc uniquement acceptable en développement. Voir Tirage Chainlink VRF.
L'image Docker
Étapes du Dockerfile :
| Étape | Effet |
|---|---|
Base node:22-bookworm-slim |
Modifiable par l'argument NODE_IMAGE |
npm ci avec cache |
Dépendances figées par package-lock.json |
Copie de hardhat.config.ts, tsconfig.json, contracts/, scripts/, docker/start-local-chain.sh |
Les tests et les artefacts locaux sont exclus par .dockerignore |
npx hardhat build |
Contrats compilés dans l'image |
USER node |
Le conteneur ne tourne pas en root |
EXPOSE 8545 |
Port RPC |
HEALTHCHECK |
Sain quand /tmp/blockchain-ready existe, vérifié toutes les 5 secondes, 60 secondes de grâce, 20 essais |
Le script de démarrage docker/start-local-chain.sh :
- lance
hardhat node --hostname 0.0.0.0 --port 8545en arrière-plan ; - interroge
eth_chainIdchaque seconde jusqu'à ce que le nœud réponde ; - lance
deploy-local.ts, puis crée/tmp/blockchain-ready, ce qui rend le conteneur sain ; - lance l'oracle local en arrière-plan ;
- attend la fin du nœud et arrête l'oracle à la réception de
INTouTERM.
Le service compose ajoute cap_drop: [ALL], no-new-privileges et restart: unless-stopped. Cette image sert au développement et à la CI ; elle n'est jamais déployée en production (voir Images Docker).
Variables
| Variable | Lue par | Défaut | Effet |
|---|---|---|---|
LOCAL_RPC_URL |
hardhat.config.ts |
http://127.0.0.1:8545 |
URL du réseau localhost |
OPERATOR_ADDRESS |
deploy-local.ts |
vide | Opérateur à habiliter et approvisionner |
OPERATOR_PRIVATE_KEY |
deploy-local.ts |
vide | Alternative : l'adresse est dérivée de la clé ; sans les deux, l'opérateur est le compte 0 |
PAUSER_ADDRESS |
deploy-local.ts |
opérateur | Titulaire de PAUSER_ROLE |
LOCAL_ADMIN_ACCOUNT_INDEX |
deploy-local.ts |
2 |
Compte administrateur et propriétaire VRF |
LOCAL_TREASURY_ACCOUNT_INDEX |
deploy-local.ts |
1 |
Compte de trésorerie |
LOCAL_STRICT_ROLES |
deploy-local.ts |
vide | true : l'opérateur ne garde pas DEFAULT_ADMIN_ROLE |
OPERATOR_FUNDING_ETH |
deploy-local.ts |
1000 |
Ethers envoyés à l'opérateur |
POAP_BASE_URI |
deploy-local.ts |
http://localhost:2053/api/v1/lottery/poap/ |
Préfixe des métadonnées des badges |
TICKET_PRICE_WEI |
lib/deployment.ts |
10000000000000000 |
Prix du ticket |
MAX_TICKETS |
lib/deployment.ts |
10000 |
Plafond de tickets par tour |
MIN_TICKETS_TO_DRAW |
lib/deployment.ts |
2 |
Minimum pour tirer |
MAX_TICKETS_PER_PLAYER |
lib/deployment.ts |
1 |
Plafond par participant ; le backend refuse aussi un second ticket |
ROUND_DURATION_SECONDS |
lib/deployment.ts |
1800 en local |
Durée d'un tour |
MUNICIPALITY_FEE_BPS |
lib/deployment.ts |
1000 |
Commission en points de base |
DEPLOYMENTS_DIR |
lib/deployment.ts |
smart-contract/deployments |
Dossier des fichiers de déploiement |
VRF_ORACLE_ACCOUNT_INDEX |
local-vrf-responder.ts |
9 |
Compte signataire de l'oracle |
VRF_POLL_INTERVAL_MS |
local-vrf-responder.ts |
2000 |
Période de scrutation |
Les variables numériques de lib/deployment.ts, sauf TICKET_PRICE_WEI, doivent être des entiers strictement positifs, sinon le script s'arrête.
Exemple : un tour de 5 minutes à 2 tickets minimum, pour une démonstration.
ROUND_DURATION_SECONDS=300 MIN_TICKETS_TO_DRAW=2 npm run deploy:localBrancher le backend
Le backend lit RPC_URL, CHAIN_ID, LOTTERY_CONTRACT_ADDRESS, POAP_CONTRACT_ADDRESS et RELAYER_PRIVATE_KEY. Leurs valeurs par défaut correspondent exactement à cette chaîne locale : http://127.0.0.1:8545, 31337, les deux adresses ci-dessus et la clé du compte 0. Dans Docker Compose, RPC_URL vaut http://blockchain:8545. Voir Variables d'environnement et Mode onchain ou simulé.
Vérifier que tout fonctionne
Depuis smart-contract/, la console Hardhat interroge directement le contrat déployé :
npx hardhat console --network localhostconst { ethers } = await network.create();
const lottery = await ethers.getContractAt("BlockLucky", "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9");
await lottery.currentRoundId();
await lottery.getRound(await lottery.currentRoundId());Les journaux du conteneur indiquent les adresses déployées et chaque réponse de l'oracle :
docker compose logs -f blockchain