Provably fair, cryptographically secure winner selection using public randomness beacons.
CryptoFairPicker is a .NET library that provides transparent, verifiable, and unbiased winner selection for lotteries, raffles, games, and any scenario where fairness matters. By default, it uses drand β a distributed public randomness beacon β enabling anyone to verify that selections are fair and unpredictable.
- Public Verifiability: Uses drand's public randomness beacon β anyone can verify your draws
- Deterministic & Reproducible: Same round + participants = same winner, every time
- Cryptographically Secure: SHA-256 hashing with rejection sampling for uniform distribution
- Pre-announcement Support: Commit to a future round before it's published
- Zero Trust Required: External randomness source eliminates "rigged draw" concerns
- Production Ready: Includes retry logic, timeouts, and comprehensive error handling
dotnet add package Tricksfor.CryptoFairPickerOr add directly to your .csproj:
<PackageReference Include="Tricksfor.CryptoFairPicker" Version="9.*.*" />using CryptoFairPicker.Extensions;
using CryptoFairPicker.Interfaces;
using CryptoFairPicker.Models;
using Microsoft.Extensions.DependencyInjection;
// Setup DI with drand
var services = new ServiceCollection();
services.AddCryptoFairPickerDrand();
var provider = services.BuildServiceProvider();
// Get the winner selector
var selector = provider.GetRequiredService<IWinnerSelector>();
// Pick a winner for round 9000000 with 100 participants
var round = RoundId.FromRound(9000000);
var winner = await selector.PickWinnerAsync(100, round);
Console.WriteLine($"Winner: Participant #{winner}");
// Output: Winner: Participant #42 (example β actual result is deterministic){
"CryptoFairPicker": {
"Strategy": "drand",
"Drand": {
"BaseUrl": "https://api.drand.sh/public",
"Chain": "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
"TimeoutSeconds": 10,
"RetryCount": 3
}
}
}var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddCryptoFairPicker(builder.Configuration);Drand is a distributed network that produces verifiable, unpredictable, and bias-resistant randomness every 3 seconds. Each "round" contains:
- Round number: Sequential identifier
- Randomness: 32 bytes of unbiased random data (hex-encoded)
- Signature: BLS signature proving authenticity
- Timestamp: When the round was created
CryptoFairPicker takes drand randomness and derives a winner:
- Fetch randomness for a specific round from drand HTTP API
- Hash the randomness using SHA-256 to derive a 32-byte block
- Map to the desired range [1, n] using rejection sampling (avoids modulo bias)
- Return the winner number
Anyone can verify the selection:
# Fetch the randomness for round 9000000
curl https://api.drand.sh/public/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/9000000
# Use CryptoFairPicker to reproduce the winner
dotnet run -- --round 9000000 --participants 100See docs/VERIFY.md for detailed verification steps.
Identifies a specific randomness round from drand:
using CryptoFairPicker.Models;
// From round number
var round = RoundId.FromRound(9000000);
// From a point in time (uses drand quicknet chain parameters)
var drawTime = DateTimeOffset.UtcNow.AddHours(1);
var futureRound = RoundId.FromTime(drawTime);
// Get the estimated publication time for a round
var publishTime = futureRound.GetEstimatedTime();
Console.WriteLine($"Round {futureRound.Value} will be published at {publishTime:O}");
// From string
var round = new RoundId("9000000");
// Parse round number
if (round.TryGetRoundNumber(out long roundNum))
{
Console.WriteLine($"Round: {roundNum}");
}Primary interface for selecting winners (1-indexed):
using CryptoFairPicker.Interfaces;
using CryptoFairPicker.Models;
public interface IWinnerSelector
{
int PickWinner(int n, RoundId round);
Task<int> PickWinnerAsync(int n, RoundId round, CancellationToken cancellationToken = default);
}n: Number of participants (must be positive)round: The randomness round to use- Returns: Winner number in range [1, n] (1-indexed)
Lower-level interface for random number generation (0-indexed):
using CryptoFairPicker.Interfaces;
using CryptoFairPicker.Models;
public interface IFairRandomSource
{
int NextInt(int toExclusive, RoundId round);
Task<int> NextIntAsync(int toExclusive, RoundId round, CancellationToken cancellationToken = default);
}Uses public randomness from the drand network:
using CryptoFairPicker.Extensions;
services.AddCryptoFairPickerDrand(options =>
{
options.BaseUrl = "https://api.drand.sh/public";
options.Chain = "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971";
options.TimeoutSeconds = 10;
options.RetryCount = 3;
});Benefits:
- Publicly verifiable randomness
- No trust required in the operator
- Perfect for high-stakes or public draws
- Supports pre-announcement
Considerations:
- Requires internet access
- Depends on drand network availability
- 3-second round period (for quicknet chain)
Uses local cryptographically secure random number generator:
using CryptoFairPicker.Extensions;
services.AddCryptoFairPickerCsprng();Benefits:
- Fast and local (no network required)
- Cryptographically secure
- No external dependencies
Considerations:
- Not deterministic (RoundId is ignored)
- Cannot be verified by third parties
- Trust required in the operator
Announce the round before it's published for maximum transparency:
using CryptoFairPicker.Interfaces;
using CryptoFairPicker.Models;
// Calculate the round for a future draw time
var drawTime = DateTimeOffset.UtcNow.AddHours(1);
var round = RoundId.FromTime(drawTime);
// Announce publicly
var publishTime = round.GetEstimatedTime();
Console.WriteLine($"The draw will use drand round {round.Value}");
Console.WriteLine($"Round will be published at: {publishTime:O}");
Console.WriteLine($"Verify at: https://api.drand.sh/public/.../{round.Value}");
// Wait for the round to be published...
// Perform the draw
var winner = await selector.PickWinnerAsync(100, round);using CryptoFairPicker.Drand;
using CryptoFairPicker.Extensions;
services.AddHttpClient<DrandRandomSource>(client =>
{
client.BaseAddress = new Uri("https://api.drand.sh/");
client.Timeout = TimeSpan.FromSeconds(15);
});
services.AddCryptoFairPickerDrand();Create verifiable transcripts for public draws:
using CryptoFairPicker.Interfaces;
using CryptoFairPicker.Models;
var round = RoundId.FromRound(9000000);
var participants = 100;
var winner = await selector.PickWinnerAsync(participants, round);
var transcript = $@"
Draw Transcript
===============
Date: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC
Participants: {participants}
Drand Round: {round.Value}
Drand Chain: quicknet (52db9ba7...)
Verification URL: https://api.drand.sh/public/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/{round.Value}
Winner: #{winner}
Anyone can verify this draw by:
1. Fetching randomness from the URL above
2. Running CryptoFairPicker with round {round.Value} and {participants} participants
3. Confirming the winner is #{winner}
";
Console.WriteLine(transcript);
// Save transcript to file or publish
File.WriteAllText("draw-transcript.txt", transcript);CryptoFairPicker uses rejection sampling to ensure uniform distribution without modulo bias:
// β WRONG: Modulo bias
var biased = randomValue % n;
// β
CORRECT: Rejection sampling
var maxValue = ulong.MaxValue - (ulong.MaxValue % (ulong)n);
if (randomValue < maxValue)
return (int)(randomValue % (ulong)n);
// Otherwise, try again with next valueThe library applies SHA-256 to drand's randomness for additional entropy:
var randomness = FetchFromDrand(round);
var derivedBlock = SHA256.HashData(randomness); // 32-byte block
var result = MapToRange(derivedBlock, n); // [0, n) using rejection samplingRobust error handling for network issues:
- Timeouts: Configurable timeout for HTTP requests (default 10s)
- Retries: Automatic retry with exponential backoff (default 3 attempts)
- Clear errors: Descriptive error messages when rounds don't exist or network fails
The library is organized following .NET best practices:
CryptoFairPicker/
βββ Interfaces/ # Core abstractions
β βββ IWinnerSelector.cs
β βββ IFairRandomSource.cs
β βββ IFairPicker.cs
β βββ IPickerStrategy.cs
βββ Models/ # Value objects and data models
β βββ RoundId.cs
βββ Services/ # Base service implementations
β βββ WinnerSelectorBase.cs
β βββ FairPicker.cs
βββ Extensions/ # Dependency injection extensions
β βββ ServiceCollectionExtensions.cs
β βββ WinnerSelectorServiceCollectionExtensions.cs
βββ Drand/ # Drand beacon implementation
β βββ DrandRandomSource.cs
β βββ DrandWinnerSelector.cs
β βββ DrandOptions.cs
βββ Csprng/ # CSPRNG fallback implementation
β βββ CsprngRandomSource.cs
β βββ CsprngWinnerSelector.cs
βββ Strategies/ # Legacy strategy pattern implementations
βββ DrandBeaconStrategy.cs
βββ CsprngStrategy.cs
βββ CommitRevealStrategy.cs
CryptoFairPicker.Interfaces- Core interface definitionsCryptoFairPicker.Models- Value objects likeRoundIdCryptoFairPicker.Services- Base service implementationsCryptoFairPicker.Extensions- DI registration methodsCryptoFairPicker.Drand- Drand-specific implementationsCryptoFairPicker.Csprng- Local CSPRNG implementationsCryptoFairPicker.Strategies- Legacy strategy implementations
Run the comprehensive test suite:
dotnet testThe test suite includes:
- Determinism tests: Same round + n produces same winner
- Uniformity tests: Distribution checks over many draws
- Error handling tests: Invalid inputs, network failures
- Integration tests: (optional) Live drand API calls
See samples/CryptoFairPicker.Sample for a complete working example.
Run it:
cd samples/CryptoFairPicker.Sample
dotnet runThe existing IPickerStrategy and IFairPicker interfaces remain available:
using CryptoFairPicker.Extensions;
using CryptoFairPicker.Interfaces;
// Old API still works
services.AddCryptoFairPicker(); // CSPRNG
services.AddDrandBeaconPicker(); // Drand (old API)
var picker = provider.GetRequiredService<IFairPicker>();
var winner = picker.PickWinner(10); // 0-indexed [0, 9]- BLS signature verification for paranoid mode
- Support for additional drand chains
- Batch selection (pick multiple winners)
- Integration with Azure Key Vault for secrets
- GraphQL API example
MIT License - see LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
- Drand - Distributed randomness beacon
- League of Entropy - Drand network operators
- Inspired by real-world needs for transparent, verifiable randomness
- π Documentation
- π Issue Tracker
- π¬ Discussions
Built with β€οΈ for fair and transparent randomness