Introduction
When software generates random results, one important question is whether those results can be independently verified.
This becomes particularly interesting in applications where users need confidence that a generated result was not changed after the fact. Examples include games, simulations, randomized challenges, testing systems, and other applications that depend on deterministic cryptographic inputs.
Simply displaying a result does not explain how that result was generated.
One approach is to use a provably fair design. Instead of requiring the user to blindly trust the generated result, the system provides cryptographic information that allows the result to be independently checked after the required information has been revealed.
A common design uses three values:
Server seed
Client seed
Nonce
The server initially keeps the server seed secret and publishes a cryptographic hash of it. After the relevant operation has been completed, the original server seed can be revealed. The user can then calculate the hash independently and compare it with the previously published hash.
This article demonstrates how to build the basic components of such a verification system using C# and the .NET cryptography APIs.
The implementation in this article is intentionally simplified for educational purposes. A production system may use a different input format, hashing procedure, result-conversion algorithm, or verification protocol.
What Does "Provably Fair" Mean?
The term provably fair does not mean that a user is guaranteed a particular outcome.
Instead, it describes a system where cryptographic information allows a generated result to be independently verified according to a defined algorithm.
The basic workflow looks like this:
Server generates secret seed
↓
SHA-256(server seed)
↓
Hash is published
↓
Application generates result
↓
Server seed is revealed
↓
User calculates SHA-256 again
↓
Hashes are comparedIf the calculated hash matches the previously published hash, the user can verify that the revealed server seed is the same value that was originally committed to.
This concept is closely related to a cryptographic commitment scheme.
The basic idea is simple:
Commit to a value without revealing the value itself.
A cryptographic hash makes this possible because the hash can be shared publicly while the original input remains secret.
Why Use a Server Seed Hash?
Suppose a server generates the following secret value:
my-secret-server-seed-123If the server publishes this value before generating the result, there is no meaningful commitment because everybody already knows the input.
Instead, the server calculates:
SHA256(serverSeed)The resulting value is a fixed-length hexadecimal representation.
For example:
9f86d081884c7d659a2feaa0c55ad015...The server can publish the hash while keeping the original seed private.
The important property of SHA-256 is that calculating the hash from the input is straightforward, while recovering the original input from a properly chosen hash should be computationally impractical.
Later, the server can reveal:
my-secret-server-seed-123The user can calculate:
SHA256(revealedServerSeed)and compare the result with the previously published hash.
If they match, the commitment is consistent.
Creating a SHA-256 Hash in C#
.NET provides cryptographic functionality through the:
System.Security.Cryptographynamespace.
Modern versions of .NET provide the convenient SHA256.HashData() method.
The following method calculates a SHA-256 hash for a string.
Code: Creating a SHA-256 Hash
using System;
using System.Security.Cryptography;
using System.Text;
public static class FairnessVerifier
{
public static string CalculateSha256(string value)
{
byte[] inputBytes =
Encoding.UTF8.GetBytes(value);
byte[] hashBytes =
SHA256.HashData(inputBytes);
return Convert.ToHexString(hashBytes)
.ToLowerInvariant();
}
}The method performs three main operations:
Converts the input string into UTF-8 bytes.
Calculates the SHA-256 hash.
Converts the resulting bytes into hexadecimal text.
We can test the method using a simple console application.
Code: Testing the Hash
string serverSeed =
"my-secret-server-seed-123";
string hash =
FairnessVerifier.CalculateSha256(serverSeed);
Console.WriteLine("Server Seed:");
Console.WriteLine(serverSeed);
Console.WriteLine("\nSHA-256 Hash:");
Console.WriteLine(hash);The important point is that the hash can be published without revealing the original secret value.
Verifying the Server Seed Later
Now suppose a system published a hash before generating a result:
9f86d081884c7d659a2feaa0c55ad015...After the operation has completed, the server reveals:
my-secret-server-seed-123The verifier can calculate the hash again.
Code: Verifying the Commitment
public static bool VerifyServerSeed(
string revealedServerSeed,
string previouslyPublishedHash)
{
string calculatedHash =
CalculateSha256(revealedServerSeed);
return string.Equals(
calculatedHash,
previouslyPublishedHash,
StringComparison.OrdinalIgnoreCase);
}The method returns:
truewhen the calculated hash matches the previously published hash.
A simple test looks like this:
string revealedSeed =
"my-secret-server-seed-123";
string publishedHash =
FairnessVerifier.CalculateSha256(
revealedSeed);
bool isValid =
FairnessVerifier.VerifyServerSeed(
revealedSeed,
publishedHash);
Console.WriteLine(
$"Seed verified: {isValid}");The output should be:
Seed verified: TrueWhere Does the Client Seed Come In?
Hashing the server seed verifies the server's original commitment, but a complete deterministic result-generation system can use additional inputs.
One common design uses a client seed.
The client seed provides another value that participates in the generation of the final result.
Conceptually, the system can work like this:
Server Seed
Client Seed
Nonce
↓
Cryptographic Function
↓
Deterministic Output
↓
Game ResultThe client seed can be supplied or selected by the user, depending on the application's design.
This creates an additional input into the result-generation process.
Understanding the Nonce
The third value is the nonce.
A nonce is commonly used as a counter.
For example:
Round 1 → nonce = 0
Round 2 → nonce = 1
Round 3 → nonce = 2
Round 4 → nonce = 3The nonce allows the same server seed and client seed combination to produce different deterministic outputs for different rounds.
Conceptually:
Server Seed + Client Seed + Nonce 0
↓
Result A
Server Seed + Client Seed + Nonce 1
↓
Result B
Server Seed + Client Seed + Nonce 2
↓
Result CChanging the nonce changes the cryptographic input and therefore changes the resulting hash.
Using HMAC-SHA256
A common cryptographic construction for combining a secret value with additional input is HMAC-SHA256.
HMAC stands for:
Hash-based Message Authentication Code
At a high level, HMAC can be thought of as using:
Secret Key + MessageIn our demonstration:
Server Seed = Secret Key
Client Seed + Nonce = MessageC# provides the HMACSHA256 class through the System.Security.Cryptography namespace.
Code: Generating an HMAC
using System;
using System.Security.Cryptography;
using System.Text;
public static string GenerateHmac(
string serverSeed,
string clientSeed,
long nonce)
{
string message =
$"{clientSeed}:{nonce}";
byte[] keyBytes =
Encoding.UTF8.GetBytes(serverSeed);
byte[] messageBytes =
Encoding.UTF8.GetBytes(message);
using var hmac =
new HMACSHA256(keyBytes);
byte[] hashBytes =
hmac.ComputeHash(messageBytes);
return Convert.ToHexString(hashBytes)
.ToLowerInvariant();
}We can now call the method:
string serverSeed =
"my-secret-server-seed-123";
string clientSeed =
"player-seed-456";
long nonce = 0;
string resultHash =
GenerateHmac(
serverSeed,
clientSeed,
nonce);
Console.WriteLine(resultHash);The result is deterministic.
If we provide exactly the same:
Server Seed
Client Seed
Noncethe HMAC calculation will produce the same output.
If any of these inputs changes, the output will change.
Turning the HMAC Into a Demonstration Result
A cryptographic hash is simply a sequence of bytes. An application may need to convert those bytes into a value suitable for its particular use case.
For this demonstration, we can convert part of the HMAC into a number between 0 and 100.
Important: This is only a demonstration mapping.
A real application can use a completely different result-generation algorithm.
If you are building a verifier for an existing system, you must reproduce that system's documented algorithm exactly.
Code: Converting the HMAC Into a Number
public static double ConvertHashToPercentage(
string hash)
{
// Use the first 8 hexadecimal characters.
string firstEightCharacters =
hash[..8];
// Convert hexadecimal text into an unsigned integer.
uint value =
Convert.ToUInt32(
firstEightCharacters,
16);
// Normalize the value to a range between 0 and 100.
double percentage =
value /
(double)uint.MaxValue *
100.0;
return percentage;
}We can now connect the pieces:
string serverSeed =
"my-secret-server-seed-123";
string clientSeed =
"player-seed-456";
long nonce = 0;
string hmac =
GenerateHmac(
serverSeed,
clientSeed,
nonce);
double result =
ConvertHashToPercentage(hmac);
Console.WriteLine(
$"HMAC: {hmac}");
Console.WriteLine(
$"Result: {result:F4}");Running the same inputs will always produce the same output.
This deterministic behavior is one of the most useful properties for a verification system.
Building a Complete Verification Example
We can now combine the concepts into a small C# console application.
Code: Complete Example
using System;
using System.Security.Cryptography;
using System.Text;
public static class ProvablyFairDemo
{
public static string Sha256(string value)
{
byte[] bytes =
Encoding.UTF8.GetBytes(value);
byte[] hash =
SHA256.HashData(bytes);
return Convert.ToHexString(hash)
.ToLowerInvariant();
}
public static string GenerateHmac(
string serverSeed,
string clientSeed,
long nonce)
{
string message =
$"{clientSeed}:{nonce}";
byte[] key =
Encoding.UTF8.GetBytes(serverSeed);
byte[] data =
Encoding.UTF8.GetBytes(message);
using var hmac =
new HMACSHA256(key);
byte[] hash =
hmac.ComputeHash(data);
return Convert.ToHexString(hash)
.ToLowerInvariant();
}
public static double ConvertHashToNumber(
string hash)
{
string firstEight =
hash[..8];
uint value =
Convert.ToUInt32(
firstEight,
16);
return value /
(double)uint.MaxValue *
100.0;
}
public static void Main()
{
string serverSeed =
"my-secret-server-seed-123";
string clientSeed =
"player-seed-456";
long nonce = 0;
// Step 1:
// Create the server commitment.
string serverSeedHash =
Sha256(serverSeed);
Console.WriteLine(
$"Published Seed Hash: {serverSeedHash}");
// Step 2:
// Generate the deterministic value.
string hmac =
GenerateHmac(
serverSeed,
clientSeed,
nonce);
Console.WriteLine(
$"HMAC: {hmac}");
// Step 3:
// Convert the cryptographic value
// into a demonstration result.
double result =
ConvertHashToNumber(hmac);
Console.WriteLine(
$"Generated Result: {result:F4}");
// Step 4:
// Verify the revealed server seed.
string verificationHash =
Sha256(serverSeed);
bool verified =
string.Equals(
serverSeedHash,
verificationHash,
StringComparison.OrdinalIgnoreCase);
Console.WriteLine(
$"Server Seed Verified: {verified}");
}
}The complete workflow is:
Server Seed
↓
SHA-256
↓
Commitment Hash
↓
Hash Published
Server Seed + Client Seed + Nonce
↓
HMAC-SHA256
↓
Deterministic Value
↓
Application-Specific Mapping
↓
Result
Later:
Revealed Server Seed
↓
SHA-256
↓
Compare With Original Hash
↓
VerificationWhy This Matters in Real Applications
The interesting part of this architecture is that the user does not have to rely entirely on the displayed result.
A verifier can independently perform the cryptographic calculations.
For example, a system could expose:
Server Seed Hash
Server Seed
Client Seed
NonceA C# application can take these values and reproduce the cryptographic calculation locally.
The user can then compare:
Expected Result
vs
Published ResultIf the values match according to the documented algorithm, the result can be independently verified.
This general approach can be useful when building:
Randomness verification tools
Gaming verification systems
Cryptographic testing utilities
Audit applications
API verification services
Blockchain applications
Automated QA systems
Deterministic simulation tools
The same principles can also be applied outside gaming wherever an application needs to demonstrate that a value was committed to before being revealed.
Important Security Considerations
A real implementation needs more than simply calculating hashes.
1. Never Expose the Server Seed Too Early
The server seed should remain secret until the appropriate reveal point.
If the secret is exposed before the result is generated, the commitment mechanism loses its purpose.
The basic security model is:
Before Result
↓
Keep Seed Secret
After Result
↓
Reveal Seed
Verification
↓
Compare Hashes2. Do Not Invent the Result Algorithm
A verifier should reproduce the published algorithm exactly.
For example, if a system specifies:
HMAC-SHA256(serverSeed, clientSeed + nonce)the verifier must use the same:
Input order
Encoding
Separator
Byte representation
Hash function
Conversion method
Result-mapping formula
Changing even one of these details can produce a completely different result.
This is one of the most important considerations when developing verification software.
3. Treat the Nonce as Part of the Input
Consider:
nonce = 1and:
nonce = 2These are different inputs.
Consequently, the resulting HMAC will also be different.
Therefore, the verifier should always record the exact nonce associated with the result being verified.
4. Validate Inputs Carefully
If a verification tool accepts data from users or an API, input validation is important.
For example:
if (string.IsNullOrWhiteSpace(serverSeed))
{
throw new ArgumentException(
"Server seed cannot be empty.");
}A production implementation should also validate:
Hash length
Hash format
Seed format
Client seed length
Nonce range
Required fields
Encoding assumptions
Input validation prevents invalid data from producing misleading verification results.
Improving the C# Implementation
The example above is intentionally simple.
A production application could improve the design by separating responsibilities into different classes.
For example:
FairnessVerifier
│
├── HashService
│
├── HmacService
│
├── ResultConverter
│
└── ValidationServiceThis makes the application easier to test and maintain.
Instead of placing every operation inside one class, each component can have a single responsibility.
For example:
public interface IHashService
{
string CalculateSha256(string value);
}A separate implementation could handle SHA-256 calculations.
Similarly:
public interface IHmacService
{
string Generate(
string key,
string message);
}This structure makes unit testing easier because each component can be tested independently.
Unit Testing the Verification Logic
Cryptographic verification code should be tested carefully because a small implementation mistake can completely change the result.
For example, a test can verify that the same input always produces the same SHA-256 hash.
[Fact]
public void Sha256_ShouldBeDeterministic()
{
string input =
"test-value";
string first =
FairnessVerifier.CalculateSha256(input);
string second =
FairnessVerifier.CalculateSha256(input);
Assert.Equal(first, second);
}Another test can verify that changing the input changes the hash:
[Fact]
public void Sha256_ShouldChange_WhenInputChanges()
{
string first =
FairnessVerifier.CalculateSha256(
"value-one");
string second =
FairnessVerifier.CalculateSha256(
"value-two");
Assert.NotEqual(first, second);
}These tests do not prove that the entire application is secure, but they help detect implementation errors.
From Console Application to Web API
Once the verification logic works, it can be exposed through an ASP.NET Core API.
For example, an API could accept:
{
"serverSeed": "my-secret-server-seed-123",
"clientSeed": "player-seed-456",
"nonce": 0
}and return:
{
"verified": true,
"hash": "...",
"result": 42.1234
}A browser-based application could then send the verification data to the API.
The architecture could look like:
Browser
↓
ASP.NET Core API
↓
Verification Service
↓
SHA-256 / HMAC-SHA256
↓
Verification ResultThe important design principle is to keep the cryptographic logic independent from the user interface.
That allows the same verification code to be reused in:
Console applications
ASP.NET Core APIs
Desktop applications
Automated tests
Background services
Possible Extensions
Once the basic verifier is working, several useful features can be added.
Multiple Nonces
The application could verify an entire sequence of results:
Nonce 0 → Result 1
Nonce 1 → Result 2
Nonce 2 → Result 3
Nonce 3 → Result 4Batch Verification
A user could submit multiple rounds at once and receive a verification report.
JSON Input
Instead of manually entering values, the verifier could accept a structured JSON document.
Web Interface
An ASP.NET Core application could provide a simple interface where users enter the required cryptographic values.
Verification History
The application could store previously verified results for auditing and debugging.
Automated Testing
The verification engine could be integrated into CI/CD pipelines to test deterministic result-generation implementations automatically.
Conclusion
Provably fair systems provide an interesting example of how cryptography can be applied to real-world software.
With C# and the .NET cryptography APIs, developers can implement the fundamental building blocks using standard classes such as:
SHA256
HMACSHA256
Encoding
ConvertThe basic workflow is straightforward:
Commit
↓
Hash
↓
Generate
↓
Reveal
↓
VerifyHowever, implementing a real verifier requires more than simply calculating a SHA-256 hash.
The verifier must understand the exact protocol being used, including how the server seed, client seed, nonce, and final hash are combined and how the resulting cryptographic value is converted into an application-specific result.
The demonstration in this article intentionally uses a simplified mapping so that the underlying concepts are easy to understand. A production verifier should always follow the exact documented algorithm of the system it is intended to verify.
This makes provably fair verification a useful practical C# project because it combines cryptography, deterministic algorithms, security, testing, APIs, and software architecture in a single application.
Join the conversation! Your thoughts help the community grow.