brown hole

Process

Beraplug uses a public Dune dashboard to determine which wallets are eligible and how many points they have earned for a given round. A round is defined by a round ID number, a description of the round's prize, a start time and an end time.

An offchain script is used to hash the round's metadata and eligible wallets, use that hash as the seed for a random number generator, and output the winning address associated with the chosen number.

Independent verification

Because the process is deterministic based on the inputs, anyone can verify that the winner is accurate. Use the script below locally or on a hosted node.js environment like Replit. Replace roundId, roundStart, roundEnd, and prizeDescription with the exact values from any entry from the game log. Check that the winner you compute matches the one on the log, and check the transaction links to verify the prizes have been transferred.

Please note that SQL, which Dune is built on, is not 100% deterministic due to how it handles floating point arithmetic and parallelization. In our testing, we see about a 0.07% chance of getting a different result. To account for this unlikely event, we execute the Dune query for each round at least twice. If the same winner is selected both times, we go with that. If the winners are different, we run the round a third time and use the majority winner. This increases round certainty to 99.9951%. If you perform independent verification and get a different result, please do not draw any conclusions until you see that result across multiple Dune executions.

require('dotenv').config();
const { parse } = require('csv-parse/sync');
const crypto = require('crypto');
const seedrandom = require('seedrandom');

// Replace this with your own Dune API key
const meta = {
  'x-dune-api-key': {{apiKey}}
}
const header = new Headers(meta);

// Replace these with the metadata for the Brown Hole round you want to validate
const roundId = {{roundId}};
const roundStart = '{{roundStart}}';
const roundEnd = '{{roundEnd}}';
const prizeDescription = '{{prizeDescription}}';

const params = {
  'query_parameters': {
    'starttime': roundStart,
    'endtime': roundEnd
  }
};
const body = JSON.stringify(params);

async function executeDuneQuery() {
  const executeUrl = 'https://api.dune.com/api/v1/query/3447423/execute';

  try {
    const execute = await fetch(executeUrl, {
      method: 'POST',
      headers: header,
      body: body
    });
    const executeResponse = await execute.json();
    console.log('Dune query submitted:', executeResponse.execution_id);

    await pollForCompletion(executeResponse.execution_id);
  } catch(error) {
    console.log('Error: ', error);
  }
}

async function pollForCompletion(executionId) {
  const statusUrl = `https://api.dune.com/api/v1/execution/${executionId}/status`;

  try {
    let completed = false;
    while (!completed) {
      const response = await fetch(statusUrl, { 
        method: 'GET',
        headers: header 
      });
      const statusResponse = await response.json();

      if (statusResponse.state === 'QUERY_STATE_COMPLETED') {
        completed = true;
        console.log('Dune query completed.');
        await getDuneResults(executionId);
      } else {
        console.log('Dune query still executing, waiting...');
        await new Promise(resolve => setTimeout(resolve, 5000)); // Wait for 5 seconds before polling again
      }
    }
  } catch (error) {
    console.error('Error polling for completion:', error);
  }
}

async function getDuneResults(executionId) {
  const resultsUrl = `https://api.dune.com/api/v1/execution/${executionId}/results/csv`;

  try {
    const dataResponse = await fetch(resultsUrl, { 
      method: 'GET',
      headers: header 
    });
    const data = await dataResponse.text();
    await flattenPoints(data);
  } catch (error) {
    console.log('Error getting results:', error);
  }
}

async function flattenPoints(data) {
  const records = parse(data, {
    columns: true,
    skip_empty_lines: true
  });

  let flatList = [];
  let eligibleWallets = [];
  records.forEach(record => {
    eligibleWallets.push(record.wallet);
    for (let i = 0; i < parseInt(record.points, 10); i++) {
      flatList.push(record.wallet);
    }
  });

  const flatListString = flatList.join('\n');
  const concatenatedString = `${roundId}\n${prizeDescription}\n${roundStart}\n${roundEnd}\n${eligibleWallets}`;

  const hash = crypto.createHash('sha256').update(concatenatedString).digest('hex');
  console.log('Hashed round data:', hash);
  const rng = seedrandom(hash);
  const randomValue = rng();
  console.log('Random number generated using hash as seed:', randomValue);

  const randomIndex = Math.floor(randomValue * flatList.length);
  console.log('Index corresponding to random number:', randomIndex);

  const winningAddress = flatList[randomIndex];
  console.log(`Winning address at chosen index: ${winningAddress}`);
  console.log('--------------------');
  console.log(`${winningAddress} wins ${prizeDescription} for Brown Hole round ${roundId} based on points tallied between ${roundStart} and ${roundEnd}`);
}

executeDuneQuery();

Last updated