diff --git a/common/src/main/java/bisq/common/crypto/Equihash.java b/common/src/main/java/bisq/common/crypto/Equihash.java
new file mode 100644
index 00000000000..dc191617793
--- /dev/null
+++ b/common/src/main/java/bisq/common/crypto/Equihash.java
@@ -0,0 +1,415 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.common.crypto;
+
+import bisq.common.util.Utilities;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.primitives.Bytes;
+import com.google.common.primitives.ImmutableIntArray;
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
+import com.google.common.primitives.UnsignedInts;
+
+import org.bouncycastle.crypto.digests.Blake2bDigest;
+
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+
+import java.math.BigInteger;
+
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.Iterator;
+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;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.math.BigInteger.ONE;
+
+/**
+ * An ASIC-resistant Proof-of-Work scheme based on the Generalized Birthday Problem (GBP),
+ * as described in this paper and used
+ * in ZCash and some other cryptocurrencies. Like
+ * Hashcash but unlike many other
+ * memory-hard and ASIC resistant PoW schemes, it is asymmetric, meaning that it
+ * supports fast verification. This makes it suitable for DoS attack protection.
+ *
+ * The Generalized Birthday Problem is an attempt to find 2k
+ * n-bit hashes (out of a list of length N) which XOR to zero. When N
+ * equals 21+n/(k+1), this has at least a handful of solutions on
+ * average, which can be found using Wagner's Algorithm, as described in the paper and
+ * implemented here. The rough idea is to split each hash into k+1
+ * n/(k+1)-bit blocks and place them into a table to look for collisions on a given
+ * block. All (partially) colliding pairs are XORed together and used to build a new table
+ * of roughly the same size as the last, before moving on to the next block and repeating
+ * the process until a full collision can be found. Keeping track of the tuple of hashes
+ * used to form each intermediate XOR result (which doubles in length each iteration)
+ * gives the final solution. The table-based approach needed to find solutions to the GBP
+ * makes it a memory-hard problem.
+ *
+ * In this implementation and the reference
+ * C++ implementation included with
+ * the paper, the hash function BLAKE2b is used to supply 256 bits, which is shortened and
+ * split into k+1 32-bit blocks. The blocks are masked to provide n/(k+1)
+ * bits each and n bits in total. This allows working with 32-bit integers
+ * throughout, for efficiency.
+ */
+@SuppressWarnings("UnstableApiUsage")
+public class Equihash {
+ private static final int HASH_BIT_LENGTH = 256;
+ /** Mean solution count per nonce for Equihash puzzles with unit difficulty. */
+ private static final double MEAN_SOLUTION_COUNT_PER_NONCE = 2.0;
+
+ private final int k, N;
+ private final int tableCapacity;
+ private final int inputNum, inputBits;
+ private final int[] hashUpperBound;
+
+ public Equihash(int n, int k, double difficulty) {
+ checkArgument(k > 0 && k < HASH_BIT_LENGTH / 32,
+ "Tree depth k must be a positive integer less than %s.",
+ HASH_BIT_LENGTH / 32);
+ checkArgument(n > 0 && n < HASH_BIT_LENGTH && n % (k + 1) == 0,
+ "Collision bit count n must be a positive multiple of k + 1 and less than %s.",
+ HASH_BIT_LENGTH);
+ checkArgument(n / (k + 1) < 30,
+ "Sub-collision bit count n / (k + 1) must be less than 30, got %s.",
+ n / (k + 1));
+ this.k = k;
+ inputNum = 1 << k;
+ inputBits = n / (k + 1) + 1;
+ N = 1 << inputBits;
+ tableCapacity = (int) (N * 1.1);
+ hashUpperBound = hashUpperBound(difficulty);
+ }
+
+ @VisibleForTesting
+ static int[] hashUpperBound(double difficulty) {
+ return Utilities.bytesToIntsBE(Utilities.copyRightAligned(
+ inverseDifficultyMinusOne(difficulty).toByteArray(), HASH_BIT_LENGTH / 8
+ ));
+ }
+
+ private static BigInteger inverseDifficultyMinusOne(double difficulty) {
+ checkArgument(difficulty >= 1.0, "Difficulty must be at least 1.");
+ int exponent = Math.getExponent(difficulty) - 52;
+ var mantissa = BigInteger.valueOf((long) Math.scalb(difficulty, -exponent));
+ var inverse = ONE.shiftLeft(HASH_BIT_LENGTH - exponent).add(mantissa).subtract(ONE).divide(mantissa);
+ 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) {
+ return Math.max(-MEAN_SOLUTION_COUNT_PER_NONCE / Math.log1p(-1.0 / Math.max(realDifficulty, 1.0)), 1.0);
+ }
+
+ public Puzzle puzzle(byte[] seed) {
+ return new Puzzle(seed);
+ }
+
+ public class Puzzle {
+ private final byte[] seed;
+
+ private Puzzle(byte[] seed) {
+ this.seed = seed;
+ }
+
+ @ToString
+ public class Solution {
+ private final long nonce;
+ private final int[] inputs;
+
+ private Solution(long nonce, int... inputs) {
+ this.nonce = nonce;
+ this.inputs = inputs;
+ }
+
+ public boolean verify() {
+ return withHashPrefix(seed, nonce).verify(inputs);
+ }
+
+ public byte[] serialize() {
+ int bitLen = 64 + inputNum * inputBits;
+ int byteLen = (bitLen + 7) / 8;
+
+ byte[] paddedBytes = new byte[byteLen + 3 & -4];
+ IntBuffer intBuffer = ByteBuffer.wrap(paddedBytes).asIntBuffer();
+ intBuffer.put((int) (nonce >> 32)).put((int) nonce);
+ int off = 64;
+ long buf = 0;
+
+ for (int v : inputs) {
+ off -= inputBits;
+ buf |= UnsignedInts.toLong(v) << off;
+ if (off <= 32) {
+ intBuffer.put((int) (buf >> 32));
+ buf <<= 32;
+ off += 32;
+ }
+ }
+ if (off < 64) {
+ intBuffer.put((int) (buf >> 32));
+ }
+ return (byteLen & 3) == 0 ? paddedBytes : Arrays.copyOf(paddedBytes, byteLen);
+ }
+ }
+
+ public Solution deserializeSolution(byte[] bytes) {
+ int bitLen = 64 + inputNum * inputBits;
+ int byteLen = (bitLen + 7) / 8;
+ checkArgument(bytes.length == byteLen,
+ "Incorrect solution byte length. Expected %s but got %s.",
+ byteLen, bytes.length);
+ checkArgument(byteLen == 0 || (byte) (bytes[byteLen - 1] << ((bitLen + 7 & 7) + 1)) == 0,
+ "Nonzero padding bits found at end of solution byte array.");
+
+ byte[] paddedBytes = (byteLen & 3) == 0 ? bytes : Arrays.copyOf(bytes, byteLen + 3 & -4);
+ IntBuffer intBuffer = ByteBuffer.wrap(paddedBytes).asIntBuffer();
+ long nonce = ((long) intBuffer.get() << 32) | UnsignedInts.toLong(intBuffer.get());
+ int[] inputs = new int[inputNum];
+ int off = 0;
+ long buf = 0;
+
+ for (int i = 0; i < inputs.length; i++) {
+ if (off < inputBits) {
+ buf = buf << 32 | UnsignedInts.toLong(intBuffer.get());
+ off += 32;
+ }
+ off -= inputBits;
+ inputs[i] = (int) (buf >>> off) & (N - 1);
+ }
+ return new Solution(nonce, inputs);
+ }
+
+ public Solution findSolution() {
+ Optional inputs;
+ for (int nonce = 0; ; nonce++) {
+ if ((inputs = withHashPrefix(seed, nonce).findInputs()).isPresent()) {
+ return new Solution(nonce, inputs.get());
+ }
+ }
+ }
+
+ @VisibleForTesting
+ int countAllSolutionsForNonce(long nonce) {
+ return (int) withHashPrefix(seed, nonce).streamInputsHits()
+ .map(ImmutableIntArray::copyOf)
+ .distinct()
+ .count();
+ }
+ }
+
+ private WithHashPrefix withHashPrefix(byte[] seed, long nonce) {
+ return new WithHashPrefix(Bytes.concat(seed, Longs.toByteArray(nonce)));
+ }
+
+ private class WithHashPrefix {
+ private final byte[] prefixBytes;
+
+ private WithHashPrefix(byte[] prefixBytes) {
+ this.prefixBytes = prefixBytes;
+ }
+
+ private int[] hashInputs(int... inputs) {
+ var digest = new Blake2bDigest(HASH_BIT_LENGTH);
+ digest.update(prefixBytes, 0, prefixBytes.length);
+ byte[] inputBytes = Utilities.intsToBytesBE(inputs);
+ digest.update(inputBytes, 0, inputBytes.length);
+ byte[] outputBytes = new byte[HASH_BIT_LENGTH / 8];
+ digest.doFinal(outputBytes, 0);
+ return Utilities.bytesToIntsBE(outputBytes);
+ }
+
+ Stream streamInputsHits() {
+ var table = computeAllHashes();
+ for (int i = 0; i < k; i++) {
+ table = findCollisions(table, i + 1 < k);
+ }
+ 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 findInputs() {
+ return streamInputsHits().findFirst();
+ }
+
+ private XorTable computeAllHashes() {
+ var tableValues = IntStream.range(0, N).flatMap(i -> {
+ int[] hash = hashInputs(i);
+ return IntStream.range(0, k + 2).map(j -> j <= k ? hash[j] & (N / 2 - 1) : i);
+ });
+ return new XorTable(k + 1, 1, ImmutableIntArray.copyOf(tableValues.parallel()));
+ }
+
+ private boolean testDifficultyCondition(int[] inputs) {
+ int[] difficultyHash = hashInputs(inputs);
+ return UnsignedInts.lexicographicalComparator().compare(difficultyHash, hashUpperBound) <= 0;
+ }
+
+ boolean verify(int[] inputs) {
+ if (inputs.length != inputNum || Arrays.stream(inputs).distinct().count() < inputNum) {
+ return false;
+ }
+ if (Arrays.stream(inputs).anyMatch(i -> i < 0 || i >= N)) {
+ return false;
+ }
+ if (!Arrays.equals(inputs, sortInputs(inputs))) {
+ return false;
+ }
+ if (!testDifficultyCondition(inputs)) {
+ return false;
+ }
+ int[] hashBlockSums = new int[k + 1];
+ for (int i = 0; i < inputs.length; i++) {
+ int[] hash = hashInputs(inputs[i]);
+ for (int j = 0; j <= k; j++) {
+ hashBlockSums[j] ^= hash[j] & (N / 2 - 1);
+ }
+ for (int ii = i + 1 + inputNum, j = 0; (ii & 1) == 0; ii /= 2, j++) {
+ if (hashBlockSums[j] != 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ }
+
+ private static class XorTable {
+ private final int hashWidth, indexTupleWidth, rowWidth, numRows;
+ private final ImmutableIntArray values;
+
+ XorTable(int hashWidth, int indexTupleWidth, ImmutableIntArray values) {
+ this.hashWidth = hashWidth;
+ this.indexTupleWidth = indexTupleWidth;
+ this.values = values;
+ rowWidth = hashWidth + indexTupleWidth;
+ numRows = (values.length() + rowWidth - 1) / rowWidth;
+ }
+
+ ImmutableIntArray getRow(int index) {
+ return values.subArray(index * rowWidth, index * rowWidth + hashWidth + indexTupleWidth);
+ }
+ }
+
+ private static class IntListMultimap {
+ final int[] shortLists;
+ final ListMultimap overspillMultimap;
+
+ IntListMultimap(int keyUpperBound) {
+ shortLists = new int[keyUpperBound * 4];
+ overspillMultimap = MultimapBuilder.hashKeys().arrayListValues().build();
+ }
+
+ PrimitiveIterator.OfInt get(int key) {
+ return new PrimitiveIterator.OfInt() {
+ int i;
+ Iterator overspillIterator;
+
+ private Iterator overspillIterator() {
+ if (overspillIterator == null) {
+ overspillIterator = overspillMultimap.get(key).iterator();
+ }
+ return overspillIterator;
+ }
+
+ @Override
+ public int nextInt() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ return i < 4 ? ~shortLists[key * 4 + i++] : overspillIterator().next();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return i < 4 && shortLists[key * 4 + i] < 0 || i == 4 && overspillIterator().hasNext();
+ }
+ };
+ }
+
+ // assumes non-negative values only:
+ void put(int key, int value) {
+ for (int i = 0; i < 4; i++) {
+ if (shortLists[key * 4 + i] == 0) {
+ shortLists[key * 4 + i] = ~value;
+ return;
+ }
+ }
+ overspillMultimap.put(key, value);
+ }
+ }
+
+ // Apply a single iteration of Wagner's Algorithm.
+ private XorTable findCollisions(XorTable table, boolean isPartial) {
+ int newHashWidth = isPartial ? table.hashWidth - 1 : 0;
+ int newIndexTupleWidth = table.indexTupleWidth * 2;
+ int newRowWidth = newHashWidth + newIndexTupleWidth;
+ var newTableValues = ImmutableIntArray.builder(
+ newRowWidth * (isPartial ? tableCapacity : 10));
+
+ var indexMultimap = new IntListMultimap(N / 2);
+ for (int i = 0; i < table.numRows; i++) {
+ var row = table.getRow(i);
+ var collisionIndices = indexMultimap.get(row.get(0));
+ while (collisionIndices.hasNext()) {
+ var collidingRow = table.getRow(collisionIndices.nextInt());
+ if (isPartial) {
+ for (int j = 1; j < table.hashWidth; j++) {
+ newTableValues.add(collidingRow.get(j) ^ row.get(j));
+ }
+ } else if (!collidingRow.subArray(1, table.hashWidth).equals(row.subArray(1, table.hashWidth))) {
+ continue;
+ }
+ newTableValues.addAll(collidingRow.subArray(table.hashWidth, collidingRow.length()));
+ newTableValues.addAll(row.subArray(table.hashWidth, row.length()));
+ }
+ indexMultimap.put(row.get(0), i);
+ }
+ return new XorTable(newHashWidth, newIndexTupleWidth, newTableValues.build());
+ }
+
+ private static int[] sortInputs(int[] inputs) {
+ Deque sublistStack = new ArrayDeque<>();
+ int[] topSublist;
+ for (int input : inputs) {
+ topSublist = new int[]{input};
+ while (!sublistStack.isEmpty() && sublistStack.peek().length == topSublist.length) {
+ topSublist = UnsignedInts.lexicographicalComparator().compare(sublistStack.peek(), topSublist) < 0
+ ? Ints.concat(sublistStack.pop(), topSublist)
+ : Ints.concat(topSublist, sublistStack.pop());
+ }
+ sublistStack.push(topSublist);
+ }
+ return sublistStack.pop();
+ }
+}
diff --git a/common/src/main/java/bisq/common/crypto/EquihashProofOfWorkService.java b/common/src/main/java/bisq/common/crypto/EquihashProofOfWorkService.java
new file mode 100644
index 00000000000..56aad6aa47f
--- /dev/null
+++ b/common/src/main/java/bisq/common/crypto/EquihashProofOfWorkService.java
@@ -0,0 +1,77 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.common.crypto;
+
+import com.google.common.primitives.Bytes;
+import com.google.common.primitives.Longs;
+
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class EquihashProofOfWorkService extends ProofOfWorkService {
+ /** Rough cost of two Hashcash iterations compared to solving an Equihash-90-5 puzzle of unit difficulty. */
+ private static final double DIFFICULTY_SCALE_FACTOR = 3.0e-5;
+
+ EquihashProofOfWorkService(int version) {
+ super(version);
+ }
+
+ @Override
+ public CompletableFuture mint(byte[] payload, byte[] challenge, double difficulty) {
+ double scaledDifficulty = scaledDifficulty(difficulty);
+ log.info("Got scaled & adjusted difficulty: {}", scaledDifficulty);
+
+ return CompletableFuture.supplyAsync(() -> {
+ long ts = System.currentTimeMillis();
+ byte[] seed = getSeed(payload, challenge);
+ byte[] solution = new Equihash(90, 5, scaledDifficulty).puzzle(seed).findSolution().serialize();
+ long counter = Longs.fromByteArray(Arrays.copyOf(solution, 8));
+ var proofOfWork = new ProofOfWork(payload, counter, challenge, difficulty,
+ System.currentTimeMillis() - ts, solution, getVersion());
+ log.info("Completed minting proofOfWork: {}", proofOfWork);
+ return proofOfWork;
+ });
+ }
+
+ private byte[] getSeed(byte[] payload, byte[] challenge) {
+ return Hash.getSha256Hash(Bytes.concat(payload, challenge));
+ }
+
+ @Override
+ public byte[] getChallenge(String itemId, String ownerId) {
+ String escapedItemId = itemId.replace(" ", " ");
+ String escapedOwnerId = ownerId.replace(" ", " ");
+ return Hash.getSha256Hash(escapedItemId + ", " + escapedOwnerId);
+ }
+
+ @Override
+ boolean verify(ProofOfWork proofOfWork) {
+ double scaledDifficulty = scaledDifficulty(proofOfWork.getDifficulty());
+
+ byte[] seed = getSeed(proofOfWork.getPayload(), proofOfWork.getChallenge());
+ var puzzle = new Equihash(90, 5, scaledDifficulty).puzzle(seed);
+ return puzzle.deserializeSolution(proofOfWork.getSolution()).verify();
+ }
+
+ private static double scaledDifficulty(double difficulty) {
+ return Equihash.adjustDifficulty(DIFFICULTY_SCALE_FACTOR * difficulty);
+ }
+}
diff --git a/common/src/main/java/bisq/common/crypto/HashCashService.java b/common/src/main/java/bisq/common/crypto/HashCashService.java
index 80e6f929594..a894f37fb71 100644
--- a/common/src/main/java/bisq/common/crypto/HashCashService.java
+++ b/common/src/main/java/bisq/common/crypto/HashCashService.java
@@ -22,116 +22,54 @@
import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
-import java.util.function.BiFunction;
import lombok.extern.slf4j.Slf4j;
/**
* HashCash implementation for proof of work
- * It doubles required work by difficulty increase (adding one leading zero).
+ * It doubles required work by log2Difficulty increase (adding one leading zero).
*
* See https://www.hashcash.org/papers/hashcash.pdf
*/
@Slf4j
-public class HashCashService {
- // Default validations. Custom implementations might use tolerance.
- private static final BiFunction isChallengeValid = Arrays::equals;
- private static final BiFunction isDifficultyValid = Integer::equals;
-
- public static CompletableFuture mint(byte[] payload,
- byte[] challenge,
- int difficulty) {
- return HashCashService.mint(payload,
- challenge,
- difficulty,
- HashCashService::testDifficulty);
- }
-
- public static boolean verify(ProofOfWork proofOfWork) {
- return verify(proofOfWork,
- proofOfWork.getChallenge(),
- proofOfWork.getNumLeadingZeros());
- }
-
- public static boolean verify(ProofOfWork proofOfWork,
- byte[] controlChallenge,
- int controlDifficulty) {
- return HashCashService.verify(proofOfWork,
- controlChallenge,
- controlDifficulty,
- HashCashService::testDifficulty);
+public class HashCashService extends ProofOfWorkService {
+ HashCashService() {
+ super(0);
}
- public static boolean verify(ProofOfWork proofOfWork,
- byte[] controlChallenge,
- int controlDifficulty,
- BiFunction challengeValidation,
- BiFunction difficultyValidation) {
- return HashCashService.verify(proofOfWork,
- controlChallenge,
- controlDifficulty,
- challengeValidation,
- difficultyValidation,
- HashCashService::testDifficulty);
- }
-
- private static boolean testDifficulty(byte[] result, long difficulty) {
- return HashCashService.numberOfLeadingZeros(result) > difficulty;
- }
-
-
- ///////////////////////////////////////////////////////////////////////////////////////////
- // Generic
- ///////////////////////////////////////////////////////////////////////////////////////////
-
- static CompletableFuture mint(byte[] payload,
+ @Override
+ public CompletableFuture mint(byte[] payload,
byte[] challenge,
- int difficulty,
- BiFunction testDifficulty) {
+ double difficulty) {
return CompletableFuture.supplyAsync(() -> {
long ts = System.currentTimeMillis();
- byte[] result;
+ int log2Difficulty = toNumLeadingZeros(difficulty);
+ byte[] hash;
long counter = 0;
do {
- result = toSha256Hash(payload, challenge, ++counter);
+ hash = toSha256Hash(payload, challenge, ++counter);
}
- while (!testDifficulty.apply(result, difficulty));
- ProofOfWork proofOfWork = new ProofOfWork(payload, counter, challenge, difficulty, System.currentTimeMillis() - ts);
+ while (numberOfLeadingZeros(hash) <= log2Difficulty);
+ byte[] solution = Longs.toByteArray(counter);
+ ProofOfWork proofOfWork = new ProofOfWork(payload, counter, challenge, difficulty,
+ System.currentTimeMillis() - ts, solution, 0);
log.info("Completed minting proofOfWork: {}", proofOfWork);
return proofOfWork;
});
}
- static boolean verify(ProofOfWork proofOfWork,
- byte[] controlChallenge,
- int controlDifficulty,
- BiFunction testDifficulty) {
- return verify(proofOfWork,
- controlChallenge,
- controlDifficulty,
- HashCashService.isChallengeValid,
- HashCashService.isDifficultyValid,
- testDifficulty);
- }
-
- static boolean verify(ProofOfWork proofOfWork,
- byte[] controlChallenge,
- int controlDifficulty,
- BiFunction challengeValidation,
- BiFunction difficultyValidation,
- BiFunction testDifficulty) {
- return challengeValidation.apply(proofOfWork.getChallenge(), controlChallenge) &&
- difficultyValidation.apply(proofOfWork.getNumLeadingZeros(), controlDifficulty) &&
- verify(proofOfWork, testDifficulty);
- }
-
- private static boolean verify(ProofOfWork proofOfWork, BiFunction testDifficulty) {
- byte[] hash = HashCashService.toSha256Hash(proofOfWork.getPayload(),
+ @Override
+ boolean verify(ProofOfWork proofOfWork) {
+ byte[] hash = toSha256Hash(proofOfWork.getPayload(),
proofOfWork.getChallenge(),
proofOfWork.getCounter());
- return testDifficulty.apply(hash, proofOfWork.getNumLeadingZeros());
+ return numberOfLeadingZeros(hash) > toNumLeadingZeros(proofOfWork.getDifficulty());
+ }
+
+ @Override
+ public byte[] getChallenge(String itemId, String ownerId) {
+ return getBytes(itemId + ownerId);
}
@@ -139,7 +77,7 @@ private static boolean verify(ProofOfWork proofOfWork, BiFunction>> 1);
}
+
+ // round up to nearest power-of-two and take the base-2 log
+ @VisibleForTesting
+ static int toNumLeadingZeros(double difficulty) {
+ return Math.getExponent(Math.max(Math.nextDown(difficulty), 0.5)) + 1;
+ }
}
diff --git a/common/src/main/java/bisq/common/crypto/ProofOfWork.java b/common/src/main/java/bisq/common/crypto/ProofOfWork.java
index 8f88e67a289..3fd1ab685b7 100644
--- a/common/src/main/java/bisq/common/crypto/ProofOfWork.java
+++ b/common/src/main/java/bisq/common/crypto/ProofOfWork.java
@@ -21,8 +21,6 @@
import com.google.protobuf.ByteString;
-import java.math.BigInteger;
-
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -34,53 +32,29 @@ public final class ProofOfWork implements NetworkPayload {
private final long counter;
@Getter
private final byte[] challenge;
- // We want to support BigInteger value for difficulty as well so we store it as byte array
- private final byte[] difficulty;
+ @Getter
+ private final double difficulty;
@Getter
private final long duration;
+ @Getter
+ private final byte[] solution;
+ @Getter
+ private final int version;
public ProofOfWork(byte[] payload,
long counter,
byte[] challenge,
- int difficulty,
- long duration) {
- this(payload,
- counter,
- challenge,
- BigInteger.valueOf(difficulty).toByteArray(),
- duration);
- }
-
- public ProofOfWork(byte[] payload,
- long counter,
- byte[] challenge,
- BigInteger difficulty,
- long duration) {
- this(payload,
- counter,
- challenge,
- difficulty.toByteArray(),
- duration);
- }
-
- public ProofOfWork(byte[] payload,
- long counter,
- byte[] challenge,
- byte[] difficulty,
- long duration) {
+ double difficulty,
+ long duration,
+ byte[] solution,
+ int version) {
this.payload = payload;
this.counter = counter;
this.challenge = challenge;
this.difficulty = difficulty;
this.duration = duration;
- }
-
- public int getNumLeadingZeros() {
- return new BigInteger(difficulty).intValue();
- }
-
- public BigInteger getTarget() {
- return new BigInteger(difficulty);
+ this.solution = solution;
+ this.version = version;
}
@@ -94,8 +68,10 @@ public protobuf.ProofOfWork toProtoMessage() {
.setPayload(ByteString.copyFrom(payload))
.setCounter(counter)
.setChallenge(ByteString.copyFrom(challenge))
- .setDifficulty(ByteString.copyFrom(difficulty))
+ .setDifficulty(difficulty)
.setDuration(duration)
+ .setSolution(ByteString.copyFrom(solution))
+ .setVersion(version)
.build();
}
@@ -104,8 +80,10 @@ public static ProofOfWork fromProto(protobuf.ProofOfWork proto) {
proto.getPayload().toByteArray(),
proto.getCounter(),
proto.getChallenge().toByteArray(),
- proto.getDifficulty().toByteArray(),
- proto.getDuration()
+ proto.getDifficulty(),
+ proto.getDuration(),
+ proto.getSolution().toByteArray(),
+ proto.getVersion()
);
}
@@ -114,9 +92,9 @@ public static ProofOfWork fromProto(protobuf.ProofOfWork proto) {
public String toString() {
return "ProofOfWork{" +
",\r\n counter=" + counter +
- ",\r\n numLeadingZeros=" + getNumLeadingZeros() +
- ",\r\n target=" + getTarget() +
+ ",\r\n difficulty=" + difficulty +
",\r\n duration=" + duration +
+ ",\r\n version=" + version +
"\r\n}";
}
}
diff --git a/common/src/main/java/bisq/common/crypto/ProofOfWorkService.java b/common/src/main/java/bisq/common/crypto/ProofOfWorkService.java
index c94e4e5c1ff..3042f5b048f 100644
--- a/common/src/main/java/bisq/common/crypto/ProofOfWorkService.java
+++ b/common/src/main/java/bisq/common/crypto/ProofOfWorkService.java
@@ -17,140 +17,60 @@
package bisq.common.crypto;
-import com.google.common.primitives.Longs;
+import com.google.common.base.Preconditions;
-import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import java.util.Optional;
import java.util.concurrent.CompletableFuture;
-import java.util.function.BiFunction;
-import lombok.extern.slf4j.Slf4j;
+import lombok.Getter;
-/**
- * Bitcoin-like proof of work implementation. Differs from original hashcash by using BigInteger for comparing
- * the hash result with the target difficulty to gain more fine grained control for difficulty adjustment.
- * This class provides a convenience method getDifficultyAsBigInteger(numLeadingZeros) to get values which are
- * equivalent to the hashcash difficulty values.
- *
- * See https://en.wikipedia.org/wiki/Hashcash"
- * "Unlike hashcash, Bitcoin's difficulty target does not specify a minimum number of leading zeros in the hash.
- * Instead, the hash is interpreted as a (very large) integer, and this integer must be less than the target integer."
- */
-@Slf4j
-public class ProofOfWorkService {
- // Default validations. Custom implementations might use tolerance.
- private static final BiFunction isChallengeValid = Arrays::equals;
- private static final BiFunction isTargetValid = BigInteger::equals;
-
- public static CompletableFuture mint(byte[] payload,
- byte[] challenge,
- BigInteger target) {
- return mint(payload,
- challenge,
- target,
- ProofOfWorkService::testTarget);
- }
-
- public static boolean verify(ProofOfWork proofOfWork) {
- return verify(proofOfWork,
- proofOfWork.getChallenge(),
- proofOfWork.getTarget());
- }
-
- public static boolean verify(ProofOfWork proofOfWork,
- byte[] controlChallenge,
- BigInteger controlTarget) {
- return verify(proofOfWork,
- controlChallenge,
- controlTarget,
- ProofOfWorkService::testTarget);
+public abstract class ProofOfWorkService {
+ private static class InstanceHolder {
+ private static final ProofOfWorkService[] INSTANCES = {
+ new HashCashService(),
+ new EquihashProofOfWorkService(1)
+ };
}
- public static boolean verify(ProofOfWork proofOfWork,
- byte[] controlChallenge,
- BigInteger controlTarget,
- BiFunction challengeValidation,
- BiFunction targetValidation) {
- return verify(proofOfWork,
- controlChallenge,
- controlTarget,
- challengeValidation,
- targetValidation,
- ProofOfWorkService::testTarget);
-
+ public static Optional forVersion(int version) {
+ return version >= 0 && version < InstanceHolder.INSTANCES.length ?
+ Optional.of(InstanceHolder.INSTANCES[version]) : Optional.empty();
}
- public static BigInteger getTarget(int numLeadingZeros) {
- return BigInteger.TWO.pow(255 - numLeadingZeros).subtract(BigInteger.ONE);
- }
+ @Getter
+ private final int version;
- private static boolean testTarget(byte[] result, BigInteger target) {
- return getUnsignedBigInteger(result).compareTo(target) < 0;
+ ProofOfWorkService(int version) {
+ this.version = version;
}
+ public abstract CompletableFuture mint(byte[] payload, byte[] challenge, double difficulty);
- ///////////////////////////////////////////////////////////////////////////////////////////
- // Generic
- ///////////////////////////////////////////////////////////////////////////////////////////
-
- static CompletableFuture mint(byte[] payload,
- byte[] challenge,
- BigInteger target,
- BiFunction testTarget) {
- return CompletableFuture.supplyAsync(() -> {
- long ts = System.currentTimeMillis();
- byte[] result;
- long counter = 0;
- do {
- result = toSha256Hash(payload, challenge, ++counter);
- }
- while (!testTarget.apply(result, target));
- return new ProofOfWork(payload, counter, challenge, target, System.currentTimeMillis() - ts);
- });
- }
+ abstract boolean verify(ProofOfWork proofOfWork);
- static boolean verify(ProofOfWork proofOfWork,
- byte[] controlChallenge,
- BigInteger controlTarget,
- BiFunction testTarget) {
- return verify(proofOfWork,
- controlChallenge,
- controlTarget,
- ProofOfWorkService.isChallengeValid,
- ProofOfWorkService.isTargetValid,
- testTarget);
+ public byte[] getPayload(String itemId) {
+ return itemId.getBytes(StandardCharsets.UTF_8);
}
- static boolean verify(ProofOfWork proofOfWork,
- byte[] controlChallenge,
- BigInteger controlTarget,
- BiFunction challengeValidation,
- BiFunction targetValidation,
- BiFunction testTarget) {
- return challengeValidation.apply(proofOfWork.getChallenge(), controlChallenge) &&
- targetValidation.apply(proofOfWork.getTarget(), controlTarget) &&
- verify(proofOfWork, testTarget);
- }
+ public abstract byte[] getChallenge(String itemId, String ownerId);
- private static boolean verify(ProofOfWork proofOfWork, BiFunction testTarget) {
- byte[] hash = toSha256Hash(proofOfWork.getPayload(), proofOfWork.getChallenge(), proofOfWork.getCounter());
- return testTarget.apply(hash, proofOfWork.getTarget());
+ public CompletableFuture mint(String itemId, String ownerId, double difficulty) {
+ return mint(getPayload(itemId), getChallenge(itemId, ownerId), difficulty);
}
+ public boolean verify(ProofOfWork proofOfWork,
+ String itemId,
+ String ownerId,
+ double controlDifficulty) {
- ///////////////////////////////////////////////////////////////////////////////////////////
- // Utils
- ///////////////////////////////////////////////////////////////////////////////////////////
-
- private static BigInteger getUnsignedBigInteger(byte[] result) {
- return new BigInteger(1, result);
- }
+ Preconditions.checkArgument(proofOfWork.getVersion() == version);
- private static byte[] toSha256Hash(byte[] payload, byte[] challenge, long counter) {
- byte[] preImage = org.bouncycastle.util.Arrays.concatenate(payload,
- challenge,
- Longs.toByteArray(counter));
- return Hash.getSha256Hash(preImage);
+ byte[] controlChallenge = getChallenge(itemId, ownerId);
+ return Arrays.equals(proofOfWork.getChallenge(), controlChallenge) &&
+ proofOfWork.getDifficulty() >= controlDifficulty &&
+ verify(proofOfWork);
}
}
diff --git a/common/src/main/java/bisq/common/util/PermutationUtil.java b/common/src/main/java/bisq/common/util/PermutationUtil.java
index a7c3e980231..0fb2208b5bd 100644
--- a/common/src/main/java/bisq/common/util/PermutationUtil.java
+++ b/common/src/main/java/bisq/common/util/PermutationUtil.java
@@ -22,7 +22,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
@@ -58,9 +58,9 @@ public static List getPartialList(List list, List indicesToRe
public static List findMatchingPermutation(R targetValue,
List list,
- BiFunction, Boolean> predicate,
+ BiPredicate> predicate,
int maxIterations) {
- if (predicate.apply(targetValue, list)) {
+ if (predicate.test(targetValue, list)) {
return list;
} else {
return findMatchingPermutation(targetValue,
@@ -74,7 +74,7 @@ public static List findMatchingPermutation(R targetValue,
private static List findMatchingPermutation(R targetValue,
List list,
List> lists,
- BiFunction, Boolean> predicate,
+ BiPredicate> predicate,
AtomicInteger maxIterations) {
for (int level = 0; level < list.size(); level++) {
// Test one level at a time
@@ -90,7 +90,7 @@ private static List findMatchingPermutation(R targetValue,
@NonNull
private static List checkLevel(R targetValue,
List previousLevel,
- BiFunction, Boolean> predicate,
+ BiPredicate> predicate,
int level,
int permutationIndex,
AtomicInteger maxIterations) {
@@ -106,7 +106,7 @@ private static List checkLevel(R targetValue,
if (level == 0) {
maxIterations.decrementAndGet();
// Check all permutations on this level
- if (predicate.apply(targetValue, newList)) {
+ if (predicate.test(targetValue, newList)) {
return newList;
}
} else {
diff --git a/common/src/main/java/bisq/common/util/Utilities.java b/common/src/main/java/bisq/common/util/Utilities.java
index 03b29aa35e1..fca323ab4c8 100644
--- a/common/src/main/java/bisq/common/util/Utilities.java
+++ b/common/src/main/java/bisq/common/util/Utilities.java
@@ -20,6 +20,7 @@
import org.bitcoinj.core.Utils;
import com.google.common.base.Splitter;
+import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
@@ -525,6 +526,34 @@ public static int byteArrayToInteger(byte[] bytes) {
return result;
}
+ public static byte[] copyRightAligned(byte[] src, int newLength) {
+ byte[] dest = new byte[newLength];
+ int srcPos = Math.max(src.length - newLength, 0);
+ int destPos = Math.max(newLength - src.length, 0);
+ System.arraycopy(src, srcPos, dest, destPos, newLength - destPos);
+ return dest;
+ }
+
+ public static byte[] intsToBytesBE(int[] ints) {
+ byte[] bytes = new byte[ints.length * 4];
+ int i = 0;
+ for (int v : ints) {
+ bytes[i++] = (byte) (v >> 24);
+ bytes[i++] = (byte) (v >> 16);
+ bytes[i++] = (byte) (v >> 8);
+ bytes[i++] = (byte) v;
+ }
+ return bytes;
+ }
+
+ public static int[] bytesToIntsBE(byte[] bytes) {
+ int[] ints = new int[bytes.length / 4];
+ for (int i = 0, j = 0; i < bytes.length / 4; i++) {
+ ints[i] = Ints.fromBytes(bytes[j++], bytes[j++], bytes[j++], bytes[j++]);
+ }
+ return ints;
+ }
+
// Helper to filter unique elements by key
public static Predicate distinctByKey(Function super T, Object> keyExtractor) {
Map