Vérifier soi-même
Cette page refait un tirage à la main, avec les mêmes données que le contrat. Rien à signer, rien à payer : tout est en lecture. Le tirage pris en exemple est le tirage 1 du pilote, réglé le 13 septembre 2026 (lu on-chain). Chaque commande ci-dessous a été exécutée telle quelle, et chaque sortie est la sortie réelle. Cette recette est celle du pilote ; elle sera relue et republiée au déploiement v2, page 5 pour les adresses.
0. Avant de commencer
Il vous faut un terminal et Foundry, la boîte à outils qui fournit cast : une seule commande, sur sa page
d'installation officielle, https://book.getfoundry.sh/getting-started/installation. Il vous faut aussi curl, jq et
python3. Aucun wallet, aucun jeton, aucune clé. Vérifiez :
cast --version
cast Version: 1.8.1
ou toute version plus récente.
Puis, une fois par terminal, les trois adresses, le bloc Robinhood de création du coffre (lu on-chain) et une fonction d'aide. En v2, ces lignes changeront ; les adresses à jour sont à la page 5.
RH_RPC=https://rpc.mainnet.chain.robinhood.com
VAULT=0x4aaEBA7E0Bb05FeE125a491D6Be956027bd147bd
ORACLE=0x6f6Cc580122F7f4358999D6005e6107b00fB7F64
VAULT_BLOCK=61022723
call() { cast call "$@" --rpc-url "$RH_RPC" | sed 's/ \[[^]]*\]//g'; }
La fonction call lance cast call sur le RPC public et retire l'annotation entre crochets que cast ajoute aux
grands nombres.
1. Lire le tirage
Le coffre numérote les tirages à partir de 0 ; epochId est le numéro du prochain (SPEC §4.1).
call "$VAULT" "epochId()(uint256)"
18
Ce que vous venez de lire : dix-huit tirages existent, numérotés de 0 à 17 (lu on-chain). Lisons le tirage 1, en six champs : hauteur Bitcoin engagée, horodatage du sweep, empreinte de la liste des tickets, poids total, pot en NAT, réglé ou non (SPEC §4.1).
call "$VAULT" "draws(uint256)(uint64,uint64,bytes32,uint256,uint256,bool)" 1
966788
1789265347
0xe930cf04cc8143daae7eed5667c794415bb0b45bb90bdc9e097ee23c038faad3
399999999
17145471
true
Ce que vous venez de lire : le tirage 1 est engagé sur le bloc Bitcoin 966788, son poids total est 399 999 999 tickets, son pot 17 145 471 NAT, et il est réglé (lu on-chain). Un seul participant avait déposé 0,4 sNET, donc 399 999 999 tickets à la poussière près, présence ×1,00 pour un premier tirage (SPEC §4.5). Gardez deux valeurs :
H=966788
TOTAL=399999999
2. Lire le hash
L'oracle donne le hash finalisé de cette hauteur ; zéro tant qu'il ne l'est pas (SPEC §6.3).
call "$ORACLE" "getHash(uint64)(bytes32)" $H
0x00000000000000000000cc51f989d6155b3eaff7294479396b4cfa94a5b22fd7
Comparez avec Bitcoin lui-même : cette adresse, dans un navigateur ou par curl, renvoie le hash du bloc 966788 vu
par mempool.space.
curl -s https://mempool.space/api/block-height/$H; echo
00000000000000000000cc51f989d6155b3eaff7294479396b4cfa94a5b22fd7
Les deux hash sont identiques, au préfixe 0x près. Combien de votes, et combien en fallait-il ? (SPEC §6.2)
HASH=$(call "$ORACLE" "getHash(uint64)(bytes32)" $H)
call "$ORACLE" "votes(uint64,bytes32)(uint256)" $H $HASH
call "$ORACLE" "THRESHOLD()(uint256)"
2
2
Ce que vous venez de lire : deux attestors ont voté ce hash, et deux suffisent (lu on-chain ; SPEC §9).
3. Recalculer la graine
La graine est l'empreinte keccak256 de trois valeurs mises bout à bout, dans cet ordre : le hash Bitcoin, l'adresse du coffre, le numéro du tirage (SPEC §4.6). « Bout à bout » se dit packed : 32 octets pour le hash, 20 pour l'adresse, 32 pour le numéro, 84 en tout, sans remplissage.
PACKED=$(cast abi-encode --packed "f(bytes32,address,uint256)" $HASH $VAULT 1)
echo $PACKED
SEED=$(cast keccak $PACKED)
echo $SEED
0x00000000000000000000cc51f989d6155b3eaff7294479396b4cfa94a5b22fd74aaeba7e0bb05fee125a491d6be956027bd147bd0000000000000000000000000000000000000000000000000000000000000001
0xc3c5773c3bdc9078846b17952bf6faf3793b969524bf6361460f9c02ac097559
Demandez la graine au contrat. Attention au piège : la fonction drawSeed prend ses arguments dans l'ordre « numéro
du tirage, puis hash », alors que l'empreinte se calcule sur « hash, adresse, numéro » (lu on-chain : ABI du coffre).
Les deux ordres diffèrent, c'est voulu.
call "$VAULT" "drawSeed(uint256,bytes32)(bytes32)" 1 $HASH
0xc3c5773c3bdc9078846b17952bf6faf3793b969524bf6361460f9c02ac097559
Même graine. Pourquoi l'ordre et le packed comptent : keccak256 est une empreinte, un seul octet déplacé change
tout. L'encodage standard, sans --packed, complète l'adresse à 32 octets et ajoute des en-têtes ; l'ordre inversé
met les mêmes octets ailleurs. Les deux donnent un autre nombre, vérifié :
cast keccak $(cast abi-encode "f(bytes32,address,uint256)" $HASH $VAULT 1)
cast keccak $(cast abi-encode --packed "f(uint256,address,bytes32)" 1 $VAULT $HASH)
0xca4d6b44da9866ca54f2ae41b86437de3181d6dc9b6223efd5c1ab09146ac137
0xe79b33a4639ab92e2c979adcaa151d0f2d03dea434b9b682a9a4ab28e8b1fc87
4. Recalculer les 11 lots
Pour chaque lot i de 0 à 10, le ticket gagnant est l'empreinte de la graine et de i, mise bout à bout, ramenée modulo le poids total (SPEC §4.6). Le lot 0 en entier :
RH=$(cast keccak $(cast abi-encode --packed "f(bytes32,uint256)" $SEED 0))
echo $RH
R0=$(python3 -c "print(int('$RH', 16) % $TOTAL)")
echo $R0
call "$VAULT" "drawTicketOwner(uint256,uint256)(address)" 1 $R0
0xd6895f840cfce16802611a18661a8e3eeb819b51ae311527e85f17232e9c91ec
238095494
0xc0b2a2f39ed1623358721E20827Da630d47DBcac
Ce que vous venez de lire : le lot 0 est tombé sur le ticket 238 095 494, qui appartient à l'adresse affichée (lu
on-chain). python3 calcule le reste de la division, trop grand pour le shell. La boucle ci-dessous répète les trois
calculs pour i de 0 à 10 :
for i in $(seq 0 10); do r=$(python3 -c "print(int(\"$(cast keccak $(cast abi-encode --packed "f(bytes32,uint256)" $SEED $i))\", 16) % $TOTAL)"); echo "lot $i : ticket $r -> $(call "$VAULT" "drawTicketOwner(uint256,uint256)(address)" 1 $r)"; done
lot 0 : ticket 238095494 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
lot 1 : ticket 320906337 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
lot 2 : ticket 52618665 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
lot 3 : ticket 80561575 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
lot 4 : ticket 227900349 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
lot 5 : ticket 118784715 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
lot 6 : ticket 4731785 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
lot 7 : ticket 390509937 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
lot 8 : ticket 300819091 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
lot 9 : ticket 179914001 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
lot 10 : ticket 267535986 -> 0xc0b2a2f39ed1623358721E20827Da630d47DBcac
Un seul participant, donc onze fois la même adresse. Avec plusieurs, les tickets sont rangés bout à bout dans l'ordre
de la liste figée, et drawTicketOwner rend le propriétaire de la plage qui contient le numéro (SPEC §4.6).
5. Comparer
Au pilote, le résultat officiel est l'événement DrawSettled émis au règlement, avec les onze gagnants et leurs
montants (SPEC §4.6). On le lit dans les journaux du coffre depuis son bloc de création, puis on décode sa partie
« données ». Le premier champ indexé de l'événement est le numéro du tirage, écrit sur 32 octets ; le filtre jq ne
garde que le tirage 1 :
DATA=$(cast logs --rpc-url "$RH_RPC" --from-block $VAULT_BLOCK --to-block latest --address "$VAULT" "DrawSettled(uint256,uint64,bytes32,address[],uint256[])" --json | jq -r '.[] | select(.topics[1] == "0x0000000000000000000000000000000000000000000000000000000000000001") | .data')
cast abi-decode "f()(uint64,bytes32,address[],uint256[])" $DATA | sed 's/ \[[^]]*\]//g'
966788
0x00000000000000000000cc51f989d6155b3eaff7294479396b4cfa94a5b22fd7
[0xc0b2a2f39ed1623358721E20827Da630d47DBcac, 0xc0b2a2f39ed1623358721E20827Da630d47DBcac, 0xc0b2a2f39ed1623358721E20827Da630d47DBcac, 0xc0b2a2f39ed1623358721E20827Da630d47DBcac, 0xc0b2a2f39ed1623358721E20827Da630d47DBcac, 0xc0b2a2f39ed1623358721E20827Da630d47DBcac, 0xc0b2a2f39ed1623358721E20827Da630d47DBcac, 0xc0b2a2f39ed1623358721E20827Da630d47DBcac, 0xc0b2a2f39ed1623358721E20827Da630d47DBcac, 0xc0b2a2f39ed1623358721E20827Da630d47DBcac, 0xc0b2a2f39ed1623358721E20827Da630d47DBcac]
[8487008, 848700, 848700, 848700, 848700, 848700, 848700, 848700, 848700, 848700, 848700]
Même hauteur, même hash, mêmes onze adresses que votre recalcul. Les montants suivent la cascade de la page 2 : 17 145 471 NAT de pot, 171 454 de prime keeper, commission nulle au pilote avant 30 jours, puis 8 487 008 pour le lot 0 et 848 700 pour chacun des dix autres, en NAT entiers (lu on-chain ; SPEC §4.6 ; §4.7 ; §4.8).
En v2, winnersOf(numéro du tirage) rend les onze adresses depuis l'état du contrat, l'événement restant émis (SPEC
§12.1 point 12) ; et ticketsOf(votre adresse) rend votre principal, votre bonus de présence et vos tickets courants,
calculés par le contrat (SPEC §12.1 point 11).
6. Si ça ne colle pas
Trois causes reviennent, dans cet ordre. Mauvais tirage : le hash et le poids total sont ceux d'un autre numéro,
vérifiez la section 1. Mauvais ordre d'arguments : l'empreinte se calcule sur « hash, adresse, numéro », drawSeed se
demande avec « numéro, hash ». Encodage non packed : sans --packed, la graine est fausse, la section 3 montre les
deux valeurs erronées. Si tout est bon et que ça ne colle toujours pas, c'est une anomalie : signalez-la publiquement,
commandes et sorties à l'appui.
Bitcoin choisit. Les attestors rapportent. Tout le monde vérifie.