From 6a070afc99b748695dc072b76067c17881d17c4e Mon Sep 17 00:00:00 2001
From: Herman Schoenfeld <herman@sphere10.com>
Date: Mon, 9 Sep 2024 22:49:24 +1000
Subject: [PATCH] Refactored VRF's for performance and added extensive tests

---
 src/Hydrogen/Crypto/VRF/VRF.cs                |  16 +--
 src/Hydrogen/Crypto/VRF/VerfiableRandom.cs    |  18 ++-
 .../Maths/ErrorBandEqualityComparer.cs        |  22 ++++
 tests/Hydrogen.CryptoEx.Tests/VRFTests.cs     |   2 +-
 .../VerifiableRandomTests.cs                  | 111 ++++++++++++++++++
 5 files changed, 154 insertions(+), 15 deletions(-)
 create mode 100644 src/Hydrogen/Maths/ErrorBandEqualityComparer.cs
 create mode 100644 tests/Hydrogen.CryptoEx.Tests/VerifiableRandomTests.cs

diff --git a/src/Hydrogen/Crypto/VRF/VRF.cs b/src/Hydrogen/Crypto/VRF/VRF.cs
index 306f098d..c260c072 100644
--- a/src/Hydrogen/Crypto/VRF/VRF.cs
+++ b/src/Hydrogen/Crypto/VRF/VRF.cs
@@ -13,16 +13,16 @@ namespace Hydrogen;
 
 public class VRF {
 
-	public static IVRFAlgorithm CreateCryptographicVRF(DSS signatureScheme, CHF hasher) 
-		=> new CryptographicVRF(signatureScheme, hasher);
+	public static CryptographicVRF CreateCryptographicVRF(CHF chf, DSS dss) 
+		=> new CryptographicVRF(dss, chf);
 
-	public static byte[] Generate(DSS signatureScheme, CHF hasher, ReadOnlySpan<byte> seed, IPrivateKey privateKey, out byte[] proof, ulong nonce = 0UL)
-		=> CreateCryptographicVRF(signatureScheme, hasher).Run(seed, privateKey, nonce, out proof);
+	public static byte[] Generate(CHF chf, DSS dss, ReadOnlySpan<byte> seed, IPrivateKey privateKey, out byte[] proof, ulong nonce = 0UL)
+		=> CreateCryptographicVRF(chf, dss).Run(seed, privateKey, nonce, out proof);
 
-	public static bool TryVerify(DSS signatureScheme, CHF hasher, ReadOnlySpan<byte> seed, ReadOnlySpan<byte> output, ReadOnlySpan<byte> proof, IPublicKey publicKey) 
-		=> CreateCryptographicVRF(signatureScheme, hasher).TryVerify(seed, output, proof, publicKey);
+	public static bool TryVerify(CHF chf, DSS dss, ReadOnlySpan<byte> seed, ReadOnlySpan<byte> output, ReadOnlySpan<byte> proof, IPublicKey publicKey) 
+		=> CreateCryptographicVRF(chf, dss).TryVerify(seed, output, proof, publicKey);
 
-	public static void VerifyOrThrow(DSS signatureScheme, CHF hasher, ReadOnlySpan<byte> seed, ReadOnlySpan<byte> output, ReadOnlySpan<byte> proof, IPublicKey publicKey)
-		=> CreateCryptographicVRF(signatureScheme, hasher).VerifyProofOrThrow(seed, output, proof, publicKey);
+	public static void VerifyOrThrow(CHF chf, DSS dss, ReadOnlySpan<byte> seed, ReadOnlySpan<byte> output, ReadOnlySpan<byte> proof, IPublicKey publicKey)
+		=> CreateCryptographicVRF(chf, dss).VerifyProofOrThrow(seed, output, proof, publicKey);
 
 }
diff --git a/src/Hydrogen/Crypto/VRF/VerfiableRandom.cs b/src/Hydrogen/Crypto/VRF/VerfiableRandom.cs
index cd489166..9ef98639 100644
--- a/src/Hydrogen/Crypto/VRF/VerfiableRandom.cs
+++ b/src/Hydrogen/Crypto/VRF/VerfiableRandom.cs
@@ -26,6 +26,10 @@ public class VerfiableRandom : IRandomNumberGenerator {
 
 	public byte[] VRFOutput { get; }
 
+	public VerfiableRandom(CHF chf, DSS dss, ReadOnlySpan<byte> seed, IPrivateKey privateKey, ulong nonce = 0L) 
+		: this(Hydrogen.VRF.CreateCryptographicVRF(chf, dss), seed, privateKey, nonce) {
+	}
+
 	public VerfiableRandom(CryptographicVRF vrf, ReadOnlySpan<byte> seed, IPrivateKey privateKey, ulong nonce = 0L) 
 		: this(vrf.CHF, vrf, seed, privateKey, nonce) {
 	}
@@ -39,21 +43,23 @@ public VerfiableRandom(CHF chf, IVRFAlgorithm vrf, ReadOnlySpan<byte> seed, IPri
 		_rng = new HashRandom(chf, VRFOutput);
 	}
 	
-	public VerfiableRandom(CryptographicVRF vrf, ReadOnlySpan<byte> seed, IPublicKey publicKey, byte[] unverifiedProof) 
+	public VerfiableRandom(CHF chf, DSS dss, ReadOnlySpan<byte> seed, IPublicKey publicKey, ReadOnlySpan<byte> unverifiedProof)
+		: this(Hydrogen.VRF.CreateCryptographicVRF(chf, dss), seed, publicKey, unverifiedProof) { 
+	}
+
+	public VerfiableRandom(CryptographicVRF vrf, ReadOnlySpan<byte> seed, IPublicKey publicKey, ReadOnlySpan<byte> unverifiedProof) 
 		: this(vrf.CHF, vrf, seed, vrf.CalculateOutput(unverifiedProof),  publicKey, unverifiedProof) {
 	}
 
-	public VerfiableRandom(CHF chf, IVRFAlgorithm vrf, ReadOnlySpan<byte> seed, ReadOnlySpan<byte> output, IPublicKey publicKey, byte[] unverifiedProof) {
+	public VerfiableRandom(CHF chf, IVRFAlgorithm vrf, ReadOnlySpan<byte> seed, ReadOnlySpan<byte> output, IPublicKey publicKey, ReadOnlySpan<byte> unverifiedProof) {
 		vrf.VerifyProofOrThrow(seed, output, unverifiedProof, publicKey);
 		CHF = chf;
 		VRF = vrf;
 		VRFSeed = seed.ToArray();
-		VRFProof = unverifiedProof;
+		VRFProof = unverifiedProof.ToArray();
 		VRFOutput = output.ToArray();
 		_rng = new HashRandom(chf, VRFOutput);
 	}
 
-	public byte[] NextBytes(int count) {
-		return _rng.NextBytes(count);
-	}
+	public void NextBytes(Span<byte> result) => _rng.NextBytes(result);
 }
diff --git a/src/Hydrogen/Maths/ErrorBandEqualityComparer.cs b/src/Hydrogen/Maths/ErrorBandEqualityComparer.cs
new file mode 100644
index 00000000..78c8a999
--- /dev/null
+++ b/src/Hydrogen/Maths/ErrorBandEqualityComparer.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http.Headers;
+
+namespace Hydrogen.Maths;
+
+public class ErrorBandEqualityComparer : IEqualityComparer<decimal> {
+	private readonly decimal _tolerance;
+
+	public ErrorBandEqualityComparer(decimal tolerance) {
+		if (tolerance < 0) {
+			throw new ArgumentOutOfRangeException(nameof(tolerance), "Tolerance must be non-negative.");
+		}
+		_tolerance = tolerance;
+	}
+
+	public bool Equals(decimal x, decimal y) {
+		return Math.Abs(x - y) <= _tolerance;
+	}
+
+	public int GetHashCode(decimal obj) => throw new NotSupportedException("Reason: loss of equivalence under transitivity due to the inexact nature of the comparison");
+}
\ No newline at end of file
diff --git a/tests/Hydrogen.CryptoEx.Tests/VRFTests.cs b/tests/Hydrogen.CryptoEx.Tests/VRFTests.cs
index 0fa0fbc0..e1630962 100644
--- a/tests/Hydrogen.CryptoEx.Tests/VRFTests.cs
+++ b/tests/Hydrogen.CryptoEx.Tests/VRFTests.cs
@@ -15,7 +15,7 @@ namespace Hydrogen.CryptoEx.Tests;
 public class VRFTests {
 
 	private IVRFAlgorithm BuildVRF(DSS dss, CHF chf)
-		=> VRF.CreateCryptographicVRF(dss, chf);
+		=> VRF.CreateCryptographicVRF(chf, dss);
 
 	[Test]
 	public void ValidProofAndOutputsPass(
diff --git a/tests/Hydrogen.CryptoEx.Tests/VerifiableRandomTests.cs b/tests/Hydrogen.CryptoEx.Tests/VerifiableRandomTests.cs
new file mode 100644
index 00000000..e9dff57d
--- /dev/null
+++ b/tests/Hydrogen.CryptoEx.Tests/VerifiableRandomTests.cs
@@ -0,0 +1,111 @@
+// Copyright (c) Sphere 10 Software. All rights reserved. (https://sphere10.com)
+// Author: Herman Schoenfeld
+//
+// Distributed under the MIT software license, see the accompanying file
+// LICENSE or visit http://www.opensource.org/licenses/mit-license.php.
+//
+// This notice must not be removed when duplicating this file or its contents, in whole or in part.
+
+using System;
+using Hydrogen.Maths;
+using NUnit.Framework;
+
+namespace Hydrogen.CryptoEx.Tests;
+
+[TestFixture]
+public class VerifiableRandomTests {
+
+	[Test]
+	public void Integration(
+		[Values(DSS.ECDSA_SECP256k1, DSS.ECDSA_SECP384R1, DSS.ECDSA_SECP521R1, DSS.ECDSA_SECT283K1, DSS.PQC_WAMS, DSS.PQC_WAMSSharp)]
+		DSS dss,
+
+		[Values(CHF.SHA2_256, CHF.SHA2_512, CHF.SHA3_256, CHF.Blake2b_256, CHF.Blake2b_128)]
+		CHF chf,
+
+		[Values(0UL, 1UL, 111UL)]
+		ulong nonce,
+
+		[Values(32, 256, 1024)]
+		int seedLen
+	) {
+		var rng = new Random(31337);
+		var seed = rng.NextBytes(seedLen);
+
+		var privateKey = Signers.GeneratePrivateKey(dss);
+		var publicKey = Signers.DerivePublicKey(dss, privateKey, nonce);
+
+
+		// Generate 1MB of random bytes using private key
+		var sourceGenerator = new VerfiableRandom(chf, dss, seed, privateKey, nonce);
+		var sourceBytes = sourceGenerator.NextBytes(1048576);
+
+		// Re-generate 1mb of random bytes using public key
+		var verifierGenerator = new VerfiableRandom(chf, dss, seed, publicKey, sourceGenerator.VRFProof);
+		var destBytes = verifierGenerator.NextBytes(1048576);
+
+		// Ensure bytes generated same
+		Assert.That(destBytes, Is.EqualTo(sourceBytes).Using(ByteArrayEqualityComparer.Instance));
+
+		// Ensure statistically random
+		var globalStats = new Statistics();
+		var stats = new Statistics[256];
+		for(var i = 0; i < 256; i++) {
+			globalStats.AddDatum(sourceBytes[i]);
+			stats[i] = new Statistics();
+			for (var j = 0; j < 256; j++) { 
+				stats[i].AddDatum(i == j ? 1 : 0);
+			}
+		}
+
+		// NOTE: error margin of 20 is used here but this is due to loss of precision in global stats, there's lots of doubles being aggregated
+		// the below stats are more accurate
+		Assert.That((decimal)globalStats.Mean, Is.EqualTo(128M).Using(new ErrorBandEqualityComparer(20M)), $"Expected mean 128 (+/- 20) but was {globalStats.Mean}"); 
+
+		// Ensure every byte occured with equal probability
+		var byteStatComparer = new ErrorBandEqualityComparer(0.000001M);
+		for(var i = 0; i < 256; i++) 
+			Assert.That((decimal)stats[i].Mean, Is.EqualTo(1/256M).Using(byteStatComparer), $"Byte {i} expected mean 1/256 (+/- 0.000001) but was {stats[i].Mean}");
+		
+	}
+
+
+		[Test]
+	public void IntegrationFails(
+		[Values(DSS.ECDSA_SECP256k1, DSS.ECDSA_SECP384R1, DSS.ECDSA_SECP521R1, DSS.ECDSA_SECT283K1, DSS.PQC_WAMS, DSS.PQC_WAMSSharp)]
+		DSS dss,
+
+		[Values(CHF.SHA2_256, CHF.SHA2_512, CHF.SHA3_256, CHF.Blake2b_256, CHF.Blake2b_128)]
+		CHF chf,
+
+		[Values(0UL, 1UL, 111UL)]
+		ulong nonce,
+
+		[Values(32, 256, 1024)]
+		int seedLen
+	) {
+		var rng = new Random(31337);
+		var seed = rng.NextBytes(seedLen);
+
+		var privateKey = Signers.GeneratePrivateKey(dss);
+		var publicKey = Signers.DerivePublicKey(dss, privateKey, nonce);
+
+
+		var sourceGenerator = new VerfiableRandom(chf, dss, seed, privateKey, nonce);
+		
+		// vary a seed byte randomly (try every index)
+		var badSeed = Tools.Array.Clone(sourceGenerator.VRFSeed);
+		for (var i = 0; i < seed.Length; i++) {
+			while ((badSeed[i] = rng.NextByte()) == sourceGenerator.VRFSeed[i]);
+			Assert.That(() => new VerfiableRandom(chf, dss, badSeed, publicKey, sourceGenerator.VRFProof), Throws.InvalidOperationException);
+		}
+
+		// vary proof byte randomly (try every index)
+		var badProof = Tools.Array.Clone(sourceGenerator.VRFProof);
+		for (var i = 0; i < sourceGenerator.VRFProof.Length; i++) {
+			while ((badProof[i] = rng.NextByte()) == sourceGenerator.VRFProof[i]);
+			Assert.That(() => new VerfiableRandom(chf, dss, sourceGenerator.VRFSeed, publicKey, badProof), Throws.InvalidOperationException);
+		}
+		
+	}
+}