mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-23 15:00:30 +01:00
Add difficulty adjustment & benchmarking to Equihash(Test)
Provide a utility method, 'Equihash::adjustDifficulty', to linearise and normalise the expected time taken to solve a puzzle, as a function of the provided difficulty, by taking into account the fact that there could be 0, 1, 2 or more puzzle solutions for any given nonce. (Wagner's algorithm is supposed to give 2 solutions on average, but the observed number is fewer, possibly due to duplicate removal.) For tractability, assume that the solution count has a Poisson distribution, which seems to have good agreement with the tests. Also add some (disabled) benchmarks to EquihashTest. These reveal an Equihash-90-5 solution time of ~146ms per puzzle per unit difficulty on a Core i3 laptop, with a verification time of ~50 microseconds.
This commit is contained in:
parent
c3e5dfd19e
commit
eb435a9513
2 changed files with 163 additions and 13 deletions
|
@ -43,6 +43,7 @@ import java.util.NoSuchElementException;
|
|||
import java.util.Optional;
|
||||
import java.util.PrimitiveIterator;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import lombok.ToString;
|
||||
|
||||
|
@ -80,6 +81,10 @@ import static java.math.BigInteger.ONE;
|
|||
@SuppressWarnings("UnstableApiUsage")
|
||||
public class Equihash {
|
||||
private static final int HASH_BIT_LENGTH = 256;
|
||||
/** Observed mean solution count per nonce for Equihash-n-4 puzzles with unit difficulty. */
|
||||
public static final double EQUIHASH_n_4_MEAN_SOLUTION_COUNT_PER_NONCE = 1.63;
|
||||
/** Observed mean solution count per nonce for Equihash-n-5 puzzles with unit difficulty. */
|
||||
public static final double EQUIHASH_n_5_MEAN_SOLUTION_COUNT_PER_NONCE = 1.34;
|
||||
|
||||
private final int k, N;
|
||||
private final int tableCapacity;
|
||||
|
@ -119,6 +124,13 @@ public class Equihash {
|
|||
return inverse.subtract(ONE).max(BigInteger.ZERO);
|
||||
}
|
||||
|
||||
/** Adjust the provided difficulty to take the variable number of puzzle solutions per
|
||||
* nonce into account, so that the expected number of attempts needed to solve a given
|
||||
* puzzle equals the reciprocal of the provided difficulty. */
|
||||
public static double adjustDifficulty(double realDifficulty, double meanSolutionCountPerNonce) {
|
||||
return Math.max(-meanSolutionCountPerNonce / Math.log1p(-1.0 / Math.max(realDifficulty, 1.0)), 1.0);
|
||||
}
|
||||
|
||||
public Puzzle puzzle(byte[] seed) {
|
||||
return new Puzzle(seed);
|
||||
}
|
||||
|
@ -205,6 +217,14 @@ public class Equihash {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
int countAllSolutionsForNonce(long nonce) {
|
||||
return (int) withHashPrefix(seed, nonce).streamInputsHits()
|
||||
.map(ImmutableIntArray::copyOf)
|
||||
.distinct()
|
||||
.count();
|
||||
}
|
||||
}
|
||||
|
||||
private WithHashPrefix withHashPrefix(byte[] seed, long nonce) {
|
||||
|
@ -228,20 +248,20 @@ public class Equihash {
|
|||
return Utilities.bytesToIntsBE(outputBytes);
|
||||
}
|
||||
|
||||
Optional<int[]> findInputs() {
|
||||
Stream<int[]> streamInputsHits() {
|
||||
var table = computeAllHashes();
|
||||
for (int i = 0; i < k; i++) {
|
||||
table = findCollisions(table, i + 1 < k);
|
||||
}
|
||||
for (int i = 0; i < table.numRows; i++) {
|
||||
if (table.getRow(i).stream().distinct().count() == inputNum) {
|
||||
int[] inputs = sortInputs(table.getRow(i).toArray());
|
||||
if (testDifficultyCondition(inputs)) {
|
||||
return Optional.of(inputs);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
return IntStream.range(0, table.numRows)
|
||||
.mapToObj(table::getRow)
|
||||
.filter(row -> row.stream().distinct().count() == inputNum)
|
||||
.map(row -> sortInputs(row.toArray()))
|
||||
.filter(this::testDifficultyCondition);
|
||||
}
|
||||
|
||||
Optional<int[]> findInputs() {
|
||||
return streamInputsHits().findFirst();
|
||||
}
|
||||
|
||||
private XorTable computeAllHashes() {
|
||||
|
|
|
@ -18,14 +18,23 @@
|
|||
package bisq.common.crypto;
|
||||
|
||||
import bisq.common.crypto.Equihash.Puzzle.Solution;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ConcurrentHashMultiset;
|
||||
import com.google.common.collect.ImmutableMultiset;
|
||||
import com.google.common.collect.Multiset;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import static bisq.common.crypto.Equihash.EQUIHASH_n_5_MEAN_SOLUTION_COUNT_PER_NONCE;
|
||||
import static java.lang.Double.POSITIVE_INFINITY;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
|
@ -42,13 +51,27 @@ public class EquihashTest {
|
|||
assertEquals("0083126e 978d4fdf 3b645a1c ac083126 e978d4fd f3b645a1 cac08312 6e978d4f", hub(500.0));
|
||||
assertEquals("00000000 00000000 2f394219 248446ba a23d2ec7 29af3d61 0607aa01 67dd94ca", hub(1.0e20));
|
||||
assertEquals("00000000 00000000 00000000 00000000 ffffffff ffffffff ffffffff ffffffff", hub(0x1.0p128));
|
||||
assertEquals("00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000", hub(Double.POSITIVE_INFINITY));
|
||||
assertEquals("00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000", hub(POSITIVE_INFINITY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdjustDifficulty() {
|
||||
assertEquals(1.0, Equihash.adjustDifficulty(0.0, 1.34), 0.0001);
|
||||
assertEquals(1.0, Equihash.adjustDifficulty(0.5, 1.34), 0.0001);
|
||||
assertEquals(1.0, Equihash.adjustDifficulty(1.0, 1.34), 0.0001);
|
||||
assertEquals(1.0, Equihash.adjustDifficulty(1.2, 1.34), 0.0001);
|
||||
assertEquals(1.22, Equihash.adjustDifficulty(1.5, 1.34), 0.01);
|
||||
assertEquals(1.93, Equihash.adjustDifficulty(2.0, 1.34), 0.01);
|
||||
assertEquals(2.62, Equihash.adjustDifficulty(2.5, 1.34), 0.01);
|
||||
assertEquals(3.30, Equihash.adjustDifficulty(3.0, 1.34), 0.01);
|
||||
assertEquals(134.0, Equihash.adjustDifficulty(100.0, 1.34), 1.0);
|
||||
assertEquals(Equihash.adjustDifficulty(POSITIVE_INFINITY, 1.34), POSITIVE_INFINITY, 1.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindSolution() {
|
||||
Equihash equihash = new Equihash(90, 5, 5.0);
|
||||
byte[] seed = new byte[64];
|
||||
Equihash equihash = new Equihash(90, 5, 2.0);
|
||||
byte[] seed = new byte[32];
|
||||
Solution solution = equihash.puzzle(seed).findSolution();
|
||||
|
||||
byte[] solutionBytes = solution.serialize();
|
||||
|
@ -59,6 +82,113 @@ public class EquihashTest {
|
|||
assertEquals(solution.toString(), roundTrippedSolution.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void benchmarkFindSolution() {
|
||||
// On Intel Core i3 CPU M 330 @ 2.13GHz ...
|
||||
//
|
||||
// For Equihash-90-5 with real difficulty 2.0, adjusted difficulty 1.933211354791211 ...
|
||||
// Total elapsed solution time: 292789 ms
|
||||
// Mean time to solve one puzzle: 292 ms
|
||||
// Puzzle solution time per unit difficulty: 146 ms
|
||||
//
|
||||
double adjustedDifficulty = Equihash.adjustDifficulty(2.0, EQUIHASH_n_5_MEAN_SOLUTION_COUNT_PER_NONCE);
|
||||
Equihash equihash = new Equihash(90, 5, adjustedDifficulty);
|
||||
|
||||
Stopwatch stopwatch = Stopwatch.createStarted();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
byte[] seed = Utilities.intsToBytesBE(new int[]{0, 0, 0, 0, 0, 0, 0, i});
|
||||
equihash.puzzle(seed).findSolution();
|
||||
}
|
||||
stopwatch.stop();
|
||||
var duration = stopwatch.elapsed();
|
||||
|
||||
System.out.println("For Equihash-90-5 with real difficulty 2.0, adjusted difficulty " + adjustedDifficulty + " ...");
|
||||
System.out.println("Total elapsed solution time: " + duration.toMillis() + " ms");
|
||||
System.out.println("Mean time to solve one puzzle: " + duration.dividedBy(1000).toMillis() + " ms");
|
||||
System.out.println("Puzzle solution time per unit difficulty: " + duration.dividedBy(2000).toMillis() + " ms");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void benchmarkVerify() {
|
||||
// On Intel Core i3 CPU M 330 @ 2.13GHz ...
|
||||
//
|
||||
// For Equihash-90-5 ...
|
||||
// Total elapsed verification time: 50046 ms
|
||||
// Mean time to verify one solution: 50046 ns
|
||||
//
|
||||
Equihash equihash = new Equihash(90, 5, 1.0);
|
||||
byte[] seed = new byte[32];
|
||||
Solution solution = equihash.puzzle(seed).findSolution();
|
||||
|
||||
Stopwatch stopwatch = Stopwatch.createStarted();
|
||||
for (int i = 0; i < 1_000_000; i++) {
|
||||
solution.verify();
|
||||
}
|
||||
stopwatch.stop();
|
||||
var duration = stopwatch.elapsed();
|
||||
|
||||
System.out.println("For Equihash-90-5 ...");
|
||||
System.out.println("Total elapsed verification time: " + duration.toMillis() + " ms");
|
||||
System.out.println("Mean time to verify one solution: " + duration.dividedBy(1_000_000).toNanos() + " ns");
|
||||
}
|
||||
|
||||
private static final int SAMPLE_NO = 10000;
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void solutionCountPerNonceStats() {
|
||||
// For Equihash-60-4...
|
||||
// Got puzzle solution count mean: 1.6161
|
||||
// Got expected count stats: [0 x 1987, 1 x 3210, 2 x 2595, 3 x 1398, 4 x 564, 5 x 183, 6 x 49, 7 x 11, 8 x 3]
|
||||
// Got actual count stats: [0 x 2014, 1 x 3230, 2 x 2546, 3 x 1395, 4 x 543, 5 x 191, 6 x 50, 7 x 24, 8 x 4, 9 x 3]
|
||||
//
|
||||
// For Equihash-70-4...
|
||||
// Got puzzle solution count mean: 1.6473
|
||||
// Got expected count stats: [0 x 1926, 1 x 3172, 2 x 2613, 3 x 1434, 4 x 591, 5 x 195, 6 x 53, 7 x 13, 8 x 2, 9]
|
||||
// Got actual count stats: [0 x 1958, 1 x 3172, 2 x 2584, 3 x 1413, 4 x 585, 5 x 204, 6 x 61, 7 x 17, 8 x 5, 9]
|
||||
//
|
||||
// For Equihash-90-5...
|
||||
// Got puzzle solution count mean: 1.3419
|
||||
// Got expected count stats: [0 x 2613, 1 x 3508, 2 x 2353, 3 x 1052, 4 x 353, 5 x 95, 6 x 21, 7 x 4, 8]
|
||||
// Got actual count stats: [0 x 2698, 1 x 3446, 2 x 2311, 3 x 1045, 4 x 352, 5 x 104, 6 x 33, 7 x 5, 8 x 3, 9, 10, 12]
|
||||
//
|
||||
// For Equihash-96-5...
|
||||
// Got puzzle solution count mean: 1.3363
|
||||
// Got expected count stats: [0 x 2628, 1 x 3512, 2 x 2347, 3 x 1045, 4 x 349, 5 x 93, 6 x 21, 7 x 4, 8]
|
||||
// Got actual count stats: [0 x 2708, 1 x 3409, 2 x 2344, 3 x 1048, 4 x 368, 5 x 94, 6 x 23, 7 x 6]
|
||||
//
|
||||
Equihash equihash = new Equihash(90, 5, 1.0);
|
||||
byte[] seed = new byte[32];
|
||||
|
||||
Multiset<Integer> stats = ConcurrentHashMultiset.create();
|
||||
IntStream.range(0, SAMPLE_NO).parallel().forEach(nonce ->
|
||||
stats.add(equihash.puzzle(seed).countAllSolutionsForNonce(nonce)));
|
||||
|
||||
double mean = (stats.entrySet().stream()
|
||||
.mapToInt(entry -> entry.getElement() * entry.getCount())
|
||||
.sum()) / (double) SAMPLE_NO;
|
||||
|
||||
System.out.println("For Equihash-90-5...");
|
||||
System.out.println("Got puzzle solution count mean: " + mean);
|
||||
System.out.println("Got expected count stats: " + expectedStatsFromPoissonDistribution(mean));
|
||||
System.out.println("Got actual count stats: " + stats);
|
||||
}
|
||||
|
||||
private Multiset<Integer> expectedStatsFromPoissonDistribution(double mean) {
|
||||
var setBuilder = ImmutableMultiset.<Integer>builder();
|
||||
double prob = Math.exp(-mean), roundError = 0.0;
|
||||
for (int i = 0, total = 0; total < SAMPLE_NO; i++) {
|
||||
int n = (int) (roundError + prob * SAMPLE_NO + 0.5);
|
||||
setBuilder.addCopies(i, n);
|
||||
roundError += prob * SAMPLE_NO - n;
|
||||
total += n;
|
||||
prob *= mean / (i + 1);
|
||||
}
|
||||
return setBuilder.build();
|
||||
}
|
||||
|
||||
private static String hub(double difficulty) {
|
||||
return hexString(Equihash.hashUpperBound(difficulty));
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue