Skip to content

Commit

Permalink
Refactored VRF's for performance and added extensive tests
Browse files Browse the repository at this point in the history
  • Loading branch information
HermanSchoenfeld committed Sep 9, 2024
1 parent 1d6ab9e commit 6a070af
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 15 deletions.
16 changes: 8 additions & 8 deletions src/Hydrogen/Crypto/VRF/VRF.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
18 changes: 12 additions & 6 deletions src/Hydrogen/Crypto/VRF/VerfiableRandom.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
}
Expand All @@ -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);
}
22 changes: 22 additions & 0 deletions src/Hydrogen/Maths/ErrorBandEqualityComparer.cs
Original file line number Diff line number Diff line change
@@ -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");
}
2 changes: 1 addition & 1 deletion tests/Hydrogen.CryptoEx.Tests/VRFTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
111 changes: 111 additions & 0 deletions tests/Hydrogen.CryptoEx.Tests/VerifiableRandomTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}

}
}

0 comments on commit 6a070af

Please sign in to comment.