I Played with 500,320 Numbers from Algorand VRF: Let’s See How Random They Really Are!

Vestige Labs
6 min readSep 6, 2023

--

Imagine trying to roll a dice on a glass table, where every move is seen and anticipated. That’s how randomness feels in the blockchain world — quite the challenge, huh?

First, let’s address the elephant in the room: Why is randomness on the blockchain such a head-scratcher? See, the beauty of most blockchains is that they’re transparent. Every action must be like a rehearsed Broadway performance; the same every time and anywhere, so everyone can agree. That’s super for security, but a bummer if you’re trying to get a spontaneous, unpredictable number. Kind of like asking an actor to ad-lib in the middle of “Hamilton”!

Our computers can whip up random numbers based on the whims of the processor clock time, but here’s the twist: If every computer in the network did this, each would show up to the party with a different random number. Chaos! We’d never reach consensus.

Algorand doesn’t just rely on standard tricks like pseudorandom mechanisms (which can be as shady as a rigged card game). And no, it doesn’t call upon mysterious external sources (oracles) to bring in data, which is like asking a stranger for a random card pull — trust issues, anyone?

Instead, it uses a Verifiable Random Function (VRF). Without diving into the dizzying world of cryptography (hats off to Silvio Micali & team for that), let’s snoop around like the curious cats we are.

Algorand’s secret sauce? Every block in the network generates a seed. Think of it as the starting note in a song. Alone, it’s predictable. But anyone can use an off-chain service to jazz it up, turning that note into a full-blown, unpredictable melody. Now, you might say, “Hey, isn’t that like those oracles you talked about?” Not quite. While oracles just blurt out a random tune, the VRF off-chain service crafts the melody based on the starting note, ensuring it’s truly unique.

The best part? No trust falls required. If the off-chain service tried to pull a sneaky on us, the smart contract would call them out. Like a referee with a whistle, anything shady gets no play on the field, all thanks to the vrf_verify opcode that Algorand blessed us with in September 2022.

Now, with our faith in Algorand’s trustworthiness intact, it’s time for the million-dollar question: Is it truly random or just pretending to be? Let’s find out if there’s any bias in this magic trick.

We need a smart contract first. Here is some TealScript wizardry! 🧙‍♂️🔮

import { Contract } from '@algorandfoundation/tealscript';

class VRFTest extends Contract {
// track total calls to the contract
total = GlobalStateKey<uint64>({ key: 't' });

// start counting at 0
createApplication(): void {
this.total.value = 0;
}

test(): StaticArray<uint64, 10> {
this.total.value = this.total.value + 1;
// here the magic happens - I'm asking the Randomness Beacon SC
// for random bytes - data is just some random text
const data = sendMethodCall<[uint64, byte[]], byte[]>({
name: 'must_get',
methodArgs: [
// DO NOT DO THIS - I do not care about security,
// so I'm asking for past VRF proofs. For actual usage,
// declare your round way before actually asking for proof
globals.round - 40,
// I'm sending sender address and a byte value of total SC calls
// to add more randomness & ensure different results on each call
concat(this.txn.sender, itob(this.total.value)),
],
// beacon application id
applicationID: Application.fromIndex(110096026),
fee: 0,
onCompletion: 'NoOp',
});
// I want to transform a big random string into 10 random numbers
const results: StaticArray<uint64, 10> = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
// big string becomes big number
let value = btobigint(data);
for (let i = 0; i < 10; i = i + 1) {
// modulo to get a number from 0 to 9999 inclusive
const part = value % 10000;
// i log it to fetch the generated numbers from an indexer
log(part);
results[i] = part;
// i move my number 4 digits to the right
value = value / 10000;
}
return results;
}
}

Isn’t TealScript just like that cool friend who makes complicated stuff look easy? Even if you’re new to the blockchain party, you can still dance to this beat!

Now for the cool part. Each time the test function jams, it drops 10 spicy logs per transaction. Each log is like a lottery ticket with a number between 0 and 9999. But are they really random? Let’s find out.

Time for some detective work!

With my trusty script, I donned my virtual detective hat and called the function. 50,032 times! (Costing me a little over 100 TestNet ALGOs, but hey, it’s for science! 🔬)

import { Indexer } from 'algosdk';
import fs from 'fs';

// here is my app :) feel free to check it yourself!
const appId = 306155399;

// logs are actually base64 encoded big-endian uint8 array of the value
function logToNumber(l: string): number {
try {
const buffer = Buffer.from(l, 'base64');
if (buffer.length === 0) {
return 0;
} else if (buffer.length === 1) {
return buffer.readUint8();
} else {
return buffer.readUint16BE();
}
} catch (e) {
throw new Error(`Bad number ${l}: ${e}`);
}
}

async function saveData() {
const logs: Record<string, string[]> = {};
const indexer = new Indexer('', 'https://testnet-idx.algonode.cloud', 443);

try {
let response = await indexer.lookupApplicationLogs(appId).do();
response['log-data'].forEach((l: any) => {
logs[l['txid']] = l['logs'].map(logToNumber);
});
let nextToken = response['next-token'];
// loading logs thousand by thousand
while (nextToken) {
response = await indexer
.lookupApplicationLogs(appId)
.nextToken(nextToken)
.do();
if (response['log-data']) {
response['log-data'].forEach((l: any) => {
logs[l['txid']] = l['logs'].map(logToNumber);
});
}
nextToken = response['next-token'];
console.log(Object.keys(logs).length);
}

const fileContent = Object.keys(logs)
.map(txid => {
return logs[txid].join(';');
})
.join('\n');

fs.writeFileSync('./logs.csv', fileContent);
console.log(
`Saved logs from ${Object.keys(logs).length} transactions to logs.csv`,
);
} catch (error) {
console.error('An error occurred:', error);
}
}

saveData();

After some script magic, I got a snazzy logs.csv file. It looks like this:

5195;4016;2927;1939;4960;8734;3800;7836;2617;7723
9450;8715;7741;7076;3173;4724;4012;1001;1755;1792
5489;340;898;1938;1947;2326;1974;476;1393;6699
1651;1706;7420;2661;8886;3204;3265;4522;23;7316
5243;5352;5076;1144;2426;7690;2207;8870;224;3297
7936;3977;5395;7589;8429;7873;4864;8792;2964;3694
...

But here’s the twist: I was suspicious. Are these numbers playing fair? To uncover this mystery, I brewed another spell — this time in Python!

import matplotlib.pyplot as plt
import numpy as np

def load_data(filename):
data = []
with open(filename, 'r') as f:
for line in f:
values = [int(i) for i in line.strip().split(';')]
data.append(values)
return np.array(data)

def plot_column_histogram(data, column_index):
plt.hist(data[:, column_index], bins=50, alpha=0.5, label=f"Source {column_index+1}")
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.title(f'Histogram for Source {column_index+1}')
mean = np.mean(data[:, column_index])
std = np.std(data[:, column_index])
xmin, xmax = plt.xlim()
x = np.linspace(xmin, xmax, 100)
y = ((1 / (np.sqrt(2 * np.pi) * std)) * np.exp(-0.5 * (1 / std * (x-mean))**2))
plt.plot(x, y, 'k', linewidth=2)
plt.legend()
plt.show()
Each source is a different column of data: source 1 is the first generated number in a transaction

The plot thickens (literally)! Our histogram shows no sneaky peaks. In a perfect random world, our average should be around 5000, and we’re hovering at ~4998.31. That’s like missing a bullseye by a hair’s breadth!

Want more proof? Time to summon π from its mystical realm.

def monte_carlo_test(data):
# Scale the data between 0 and 1
scaled_data = data / np.max(data)

# Take two values at a time
x_vals = scaled_data[:, 0::2]
y_vals = scaled_data[:, 1::2]

inside_circle_count = 0
total_count = 0

for x, y in zip(np.ravel(x_vals), np.ravel(y_vals)):
if x**2 + y**2 <= 1:
inside_circle_count += 1
total_count += 1

estimated_pi = 4 * (inside_circle_count / total_count)
print(f"Estimated value of pi using Monte Carlo: {estimated_pi}")

The result? A π of 3.141157659098177.
If π were a pie, this one’s baked to near perfection!

And there you have it! Trustless randomness that’s not just a myth but very much a reality! 🌟 Imagine the wild apps, especially in gaming, that’ll tap into this magic.

If you’re itching to craft some spells of your own, dive into the Algorand cauldron on Discord! The ALGO token may be on a rollercoaster, but for developers, Algorand’s like a theme park with all the best rides.

🔗 Join the Magic Circle on Discord
📖 And if you’re a TealScript fan, here’s your spellbook: TealScript Docs

Made something enchanting? Give me a shoutout on Twitter @grzracz.
I’d love to see your creations!

Happy coding!

--

--

Vestige Labs
Vestige Labs

Written by Vestige Labs

Vestige Labs is a software house for Algorand Virtual Machine.