Skip to main content

Verify a Flipper result

  1. Go to the Flipper history and click Verify.
  2. Copy the transaction hash.
  3. Paste the transaction hash in the input field below.
  4. Click Verify to double-check the Flipper result.

How does the verification process work?

The verification process involves the following steps:

  1. Fetch the transaction receipt and block information.
  2. Decode the logs to extract the drand round used by Gelato VRF and the requestId.
  3. Fetch the drand chain information to determine the randomness delay.
  4. Fetch the drand randomness and calculate the random value.
  5. Validate the drand beacon randomness.
  6. Determine the game results based on the random value and subsequent hashed values.

What is drand?

Drand is a distributed randomness beacon service that provides publicly verifiable randomness.

The drand service is used by Gelato VRF to generate randomness for Flipper. You can find out more about drand by checking out the drand and Gelato VRF documentations.

How can I verify the Flipper result via code?

The following JavaScript code snippet shows how the verification process works.

Note: The following implementation requires v6 of the ethers library.

import {
Interface,
JsonRpcProvider,
AbiCoder,
keccak256,
solidityPackedKeccak256,
} from "ethers";
import { verifyBeacon } from "drand-client/beacon-verification";

// Configuration constants
const CONFIG = {
BLAST_CHAIN_ID: 81457,
GELATO_VRF_CONSUMER: "0x95c68c52bb12a43069973FDCD88e4e93d2142f10",
DRAND_API_URL: "https://drand.cloudflare.com/",
DRAND_CHAIN_HASH:
"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
FLIPPER_CONTRACT_ADDRESS: "0x96648d17c273A932197aCF2232653Bed7D69EC6f",
BLAST_PROVIDER_URL: "https://rpc.blast.io",
};

// Custom error class for invalid input
class InvalidInput extends Error {
constructor() {
super(
"Transaction not supported. \n\nPlease ensure the transaction hash references a valid randomness request transaction for Flipper.",
);
this.name = "InvalidInput";
}
}

// Setup the contract interface, provider and contract instance
const flipperInterface = new Interface([
"event RequestedRandomness(uint256 drandRoundId, bytes data)",
"event Flipper__GameCreated(uint256 blockNumber, address player, uint256 numberOfRounds, uint256 playAmountPerRound, address currency, int256 stopGain, int256 stopLoss, bool isGold)",
]);
const blastProvider = new JsonRpcProvider(CONFIG.BLAST_PROVIDER_URL);

/**
* Encodes and hashes the provided data.
* @param {string[]} dataTypes Types of data to encode
* @param {string[]} values Values to encode
* @returns {string} The keccak256 hash of the encoded data
*/
function encodeAndHash(dataTypes, values) {
const abiCoder = new AbiCoder();
const encoded = abiCoder.encode(dataTypes, values);
return keccak256(encoded);
}

/**
* Processes the drand randomness and request id to generate the random value used.
* @param {string} drandRandomness The randomness from drand
* @param {number} requestId The request id for randomness
* @returns {bigint} The calculated random value
*/
function retrieveRandomValue(drandRandomness, requestId) {
const firstHash = encodeAndHash(
["uint256", "address", "uint256", "uint256"],
[
`0x${drandRandomness}`,
CONFIG.GELATO_VRF_CONSUMER,
CONFIG.BLAST_CHAIN_ID,
requestId,
],
);
const secondHash = encodeAndHash(["uint256", "uint256"], [firstHash, 0]);
return BigInt(secondHash);
}

/**
* Verifies the Flipper result for a given transaction hash.
* @param {string} playTransactionHash The play transaction hash to verify
* @returns {Promise<{isValidBeacon: boolean, differenceSeconds: number, roundId: number, randomValue: bigint, expectedResults: string[]}>} The result of the verification process
*/
export async function verifyFlipperResult(playTransactionHash) {
const requestReceipt = await blastProvider.getTransactionReceipt(
playTransactionHash,
);

if (!requestReceipt) {
throw new InvalidInput();
}

const to = requestReceipt.to;

if (CONFIG.FLIPPER_CONTRACT_ADDRESS !== to) {
throw new InvalidInput();
}

const block = await blastProvider.getBlock(requestReceipt.blockNumber);

const randomnessRequestLog = requestReceipt.logs.find(
(log) =>
log.topics[0] ===
"0xd91fc3685b930310b008ec37d2334870cab88a023ed8cc628a2e2ccd4e55d202",
);

const gameCreatedLog = requestReceipt.logs.find(
(log) =>
log.topics[0] ===
"0xcde301d58079808ad89871a77ae0d9755d03300034b5ff0bd49bf6901d3bd40e",
);

if (!randomnessRequestLog || !gameCreatedLog) {
throw new InvalidInput();
}

const { drandRoundId, data } = flipperInterface.decodeEventLog(
"RequestedRandomness",
randomnessRequestLog.data,
randomnessRequestLog.topics,
);

const { numberOfRounds, isGold } = flipperInterface.decodeEventLog(
"Flipper__GameCreated",
gameCreatedLog.data,
gameCreatedLog.topics,
);

const abiCoder = new AbiCoder();
const [requestId] = abiCoder.decode(["uint256", "bytes"], data);

// Fetch drand chain info
const drandChainInfoRes = await fetch(
`${CONFIG.DRAND_API_URL}/${CONFIG.DRAND_CHAIN_HASH}/info`,
);
const chainInfo = await drandChainInfoRes.json();
const drandRoundTimestamp =
Number(drandRoundId) * chainInfo.period + chainInfo.genesis_time;
const differenceSeconds = drandRoundTimestamp - block.timestamp;

// Fetch drand randomness
const drandRandomnessRes = await fetch(
`${CONFIG.DRAND_API_URL}/${CONFIG.DRAND_CHAIN_HASH}/public/${drandRoundId}`,
);
const beacon = await drandRandomnessRes.json();
const randomValue = retrieveRandomValue(beacon.randomness, requestId);

// Verify the beacon randomness
const isValidBeacon = await verifyBeacon(
chainInfo,
beacon,
Number(drandRoundId),
);

const expectedResults = [];
let runningGameStateRandomWord = randomValue;
for (let i = 0; i < numberOfRounds; i++) {
expectedResults.push(
runningGameStateRandomWord % 2n != 0n ? "Gold" : "Silver",
);
runningGameStateRandomWord = BigInt(
solidityPackedKeccak256(["uint256"], [runningGameStateRandomWord]),
);
}

return {
isValidBeacon,
differenceSeconds,
isGold,
randomValue,
expectedResults,
};
}

// Example usage
verifyFlipperResult("TRANSACTION_HASH").catch(console.error);