Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide ASIC resistant PoW scheme for BSQ swaps #5858

Merged
merged 15 commits into from Dec 7, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 30 additions & 10 deletions common/src/main/java/bisq/common/crypto/Equihash.java
Expand Up @@ -43,6 +43,7 @@
import java.util.Optional;
import java.util.PrimitiveIterator;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import lombok.ToString;

Expand Down Expand Up @@ -80,6 +81,10 @@
@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;
Expand Down Expand Up @@ -119,6 +124,13 @@ private static BigInteger inverseDifficultyMinusOne(double difficulty) {
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);
}
Expand Down Expand Up @@ -205,6 +217,14 @@ public Solution findSolution() {
}
}
}

@VisibleForTesting
int countAllSolutionsForNonce(long nonce) {
return (int) withHashPrefix(seed, nonce).streamInputsHits()
.map(ImmutableIntArray::copyOf)
.distinct()
.count();
}
}

private WithHashPrefix withHashPrefix(byte[] seed, long nonce) {
Expand All @@ -228,20 +248,20 @@ private int[] hashInputs(int... inputs) {
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() {
Expand Down
Expand Up @@ -98,7 +98,7 @@ static CompletableFuture<ProofOfWork> mint(byte[] payload,
result = toSha256Hash(payload, challenge, ++counter);
}
while (!testDifficulty.test(result, difficulty));
ProofOfWork proofOfWork = new ProofOfWork(payload, counter, challenge, difficulty, System.currentTimeMillis() - ts);
ProofOfWork proofOfWork = new ProofOfWork(payload, counter, challenge, difficulty, System.currentTimeMillis() - ts, 0);
log.info("Completed minting proofOfWork: {}", proofOfWork);
return proofOfWork;
});
Expand Down
36 changes: 21 additions & 15 deletions common/src/main/java/bisq/common/crypto/ProofOfWork.java
Expand Up @@ -38,51 +38,55 @@ public final class ProofOfWork implements NetworkPayload {
private final byte[] difficulty;
@Getter
private final long duration;
@Getter
private final int version;

public ProofOfWork(byte[] payload,
long counter,
byte[] challenge,
int difficulty,
long duration) {
long duration,
int version) {
this(payload,
counter,
challenge,
BigInteger.valueOf(difficulty).toByteArray(),
duration);
duration,
version);
}

public ProofOfWork(byte[] payload,
long counter,
byte[] challenge,
BigInteger difficulty,
long duration) {
long duration,
int version) {
this(payload,
counter,
challenge,
difficulty.toByteArray(),
duration);
duration,
version);
}

public ProofOfWork(byte[] payload,
long counter,
byte[] challenge,
byte[] difficulty,
long duration) {
private ProofOfWork(byte[] payload,
long counter,
byte[] challenge,
byte[] difficulty,
long duration,
int version) {
this.payload = payload;
this.counter = counter;
this.challenge = challenge;
this.difficulty = difficulty;
this.duration = duration;
this.version = version;
}

public int getNumLeadingZeros() {
return new BigInteger(difficulty).intValue();
}

public BigInteger getTarget() {
return new BigInteger(difficulty);
}


///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
Expand All @@ -96,6 +100,7 @@ public protobuf.ProofOfWork toProtoMessage() {
.setChallenge(ByteString.copyFrom(challenge))
.setDifficulty(ByteString.copyFrom(difficulty))
.setDuration(duration)
.setVersion(version)
.build();
}

Expand All @@ -105,7 +110,8 @@ public static ProofOfWork fromProto(protobuf.ProofOfWork proto) {
proto.getCounter(),
proto.getChallenge().toByteArray(),
proto.getDifficulty().toByteArray(),
proto.getDuration()
proto.getDuration(),
proto.getVersion()
);
}

Expand All @@ -115,8 +121,8 @@ public String toString() {
return "ProofOfWork{" +
",\r\n counter=" + counter +
",\r\n numLeadingZeros=" + getNumLeadingZeros() +
",\r\n target=" + getTarget() +
",\r\n duration=" + duration +
",\r\n version=" + version +
"\r\n}";
}
}
136 changes: 133 additions & 3 deletions common/src/test/java/bisq/common/crypto/EquihashTest.java
Expand Up @@ -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;

Expand All @@ -42,13 +51,27 @@ public void testHashUpperBound() {
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();
Expand All @@ -59,6 +82,113 @@ public void testFindSolution() {
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));
}
Expand Down