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 keyExtractor) { Map map = new ConcurrentHashMap<>(); diff --git a/common/src/test/java/bisq/common/crypto/EquihashTest.java b/common/src/test/java/bisq/common/crypto/EquihashTest.java new file mode 100644 index 00000000000..16b3510ba23 --- /dev/null +++ b/common/src/test/java/bisq/common/crypto/EquihashTest.java @@ -0,0 +1,201 @@ +/* + * 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.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 java.lang.Double.POSITIVE_INFINITY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class EquihashTest { + @Test + public void testHashUpperBound() { + assertEquals("ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff", hub(1)); + assertEquals("aaaaaaaa aaaaaaaa aaaaaaaa aaaaaaaa aaaaaaaa aaaaaaaa aaaaaaaa aaaaaaaa", hub(1.5)); + assertEquals("7fffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff", hub(2)); + assertEquals("55555555 55555555 55555555 55555555 55555555 55555555 55555555 55555555", hub(3)); + assertEquals("3fffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff", hub(4)); + assertEquals("33333333 33333333 33333333 33333333 33333333 33333333 33333333 33333333", hub(5)); + assertEquals("051eb851 eb851eb8 51eb851e b851eb85 1eb851eb 851eb851 eb851eb8 51eb851e", hub(50.0)); + 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(POSITIVE_INFINITY)); + } + + @Test + public void testAdjustDifficulty() { + assertEquals(1.0, Equihash.adjustDifficulty(0.0), 0.0001); + assertEquals(1.0, Equihash.adjustDifficulty(0.5), 0.0001); + assertEquals(1.0, Equihash.adjustDifficulty(1.0), 0.0001); + assertEquals(1.0, Equihash.adjustDifficulty(1.1), 0.0001); + assertEquals(1.12, Equihash.adjustDifficulty(1.2), 0.01); + assertEquals(1.83, Equihash.adjustDifficulty(1.5), 0.01); + assertEquals(2.89, Equihash.adjustDifficulty(2.0), 0.01); + assertEquals(3.92, Equihash.adjustDifficulty(2.5), 0.01); + assertEquals(4.93, Equihash.adjustDifficulty(3.0), 0.01); + assertEquals(200.0, Equihash.adjustDifficulty(100.0), 1.5); + assertEquals(Equihash.adjustDifficulty(POSITIVE_INFINITY), POSITIVE_INFINITY, 1.0); + } + + @Test + public void testFindSolution() { + Equihash equihash = new Equihash(90, 5, 2.0); + byte[] seed = new byte[32]; + Solution solution = equihash.puzzle(seed).findSolution(); + + byte[] solutionBytes = solution.serialize(); + Solution roundTrippedSolution = equihash.puzzle(seed).deserializeSolution(solutionBytes); + + assertTrue(solution.verify()); + assertEquals(72, solutionBytes.length); + 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 2.8853900817779268 ... + // Total elapsed solution time: 279583 ms + // Mean time to solve one puzzle: 279 ms + // Puzzle solution time per unit difficulty: 139 ms + // + double adjustedDifficulty = Equihash.adjustDifficulty(2.0); + 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.9797 + // Got expected count stats: [0 x 1381, 1 x 2734, 2 x 2707, 3 x 1786, 4 x 884, 5 x 350, 6 x 115, 7 x 33, 8 x 8, 9 x 2] + // Got actual count stats: [0 x 1413, 1 x 2704, 2 x 2699, 3 x 1813, 4 x 866, 5 x 330, 6 x 115, 7 x 35, 8 x 19, 9 x 5, 10] + // + // For Equihash-70-4... + // Got puzzle solution count mean: 1.9988 + // Got expected count stats: [0 x 1355, 1 x 2708, 2 x 2707, 3 x 1803, 4 x 902, 5 x 360, 6 x 120, 7 x 34, 8 x 9, 9 x 2] + // Got actual count stats: [0 x 1362, 1 x 2690, 2 x 2720, 3 x 1826, 4 x 870, 5 x 353, 6 x 129, 7 x 41, 8 x 7, 9 x 2] + // + // For Equihash-90-5... + // Got puzzle solution count mean: 1.9921 + // Got expected count stats: [0 x 1364, 1 x 2717, 2 x 2707, 3 x 1797, 4 x 896, 5 x 356, 6 x 119, 7 x 33, 8 x 9, 9 x 2] + // Got actual count stats: [0 x 1379, 1 x 2709, 2 x 2729, 3 x 1750, 4 x 900, 5 x 362, 6 x 119, 7 x 39, 8 x 11, 9, 10] + // + // For Equihash-96-5... + // Got puzzle solution count mean: 1.9997 + // Got expected count stats: [0 x 1354, 1 x 2707, 2 x 2707, 3 x 1804, 4 x 902, 5 x 360, 6 x 121, 7 x 34, 8 x 9, 9 x 2] + // Got actual count stats: [0 x 1405, 1 x 2621, 2 x 2733, 3 x 1802, 4 x 928, 5 x 342, 6 x 123, 7 x 29, 8 x 13, 9 x 3, 10] + // + Equihash equihash = new Equihash(90, 5, 1.0); + byte[] seed = new byte[32]; + + Multiset 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 expectedStatsFromPoissonDistribution(double mean) { + var setBuilder = ImmutableMultiset.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)); + } + + private static String hexString(int[] ints) { + return Arrays.stream(ints) + .mapToObj(n -> Strings.padStart(Integer.toHexString(n), 8, '0')) + .collect(Collectors.joining(" ")); + } +} diff --git a/common/src/test/java/bisq/common/crypto/HashCashServiceTest.java b/common/src/test/java/bisq/common/crypto/HashCashServiceTest.java index 115483d25fb..22eb5d1e283 100644 --- a/common/src/test/java/bisq/common/crypto/HashCashServiceTest.java +++ b/common/src/test/java/bisq/common/crypto/HashCashServiceTest.java @@ -32,11 +32,22 @@ public void testNumberOfLeadingZeros() { assertEquals(9, HashCashService.numberOfLeadingZeros(new byte[]{Byte.parseByte("00000000", 2), Byte.parseByte("01010000", 2)})); } + @Test + public void testToNumLeadingZeros() { + assertEquals(0, HashCashService.toNumLeadingZeros(-1.0)); + assertEquals(0, HashCashService.toNumLeadingZeros(0.0)); + assertEquals(0, HashCashService.toNumLeadingZeros(1.0)); + assertEquals(1, HashCashService.toNumLeadingZeros(1.1)); + assertEquals(1, HashCashService.toNumLeadingZeros(2.0)); + assertEquals(8, HashCashService.toNumLeadingZeros(256.0)); + assertEquals(1024, HashCashService.toNumLeadingZeros(Double.POSITIVE_INFINITY)); + } + // @Ignore @Test public void testDiffIncrease() throws ExecutionException, InterruptedException { StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < 12; i++) { + for (int i = 0; i < 9; i++) { run(i, stringBuilder); } log.info(stringBuilder.toString()); @@ -58,24 +69,25 @@ public void testDiffIncrease() throws ExecutionException, InterruptedException { //Minting 1000 tokens with 13 leading zeros took 25.276 ms per token and 16786 iterations in average. Verification took 0.002 ms per token. } - private void run(int difficulty, StringBuilder stringBuilder) throws ExecutionException, InterruptedException { + private void run(int log2Difficulty, StringBuilder stringBuilder) throws ExecutionException, InterruptedException { + double difficulty = Math.scalb(1.0, log2Difficulty); int numTokens = 1000; byte[] payload = RandomStringUtils.random(50, true, true).getBytes(StandardCharsets.UTF_8); long ts = System.currentTimeMillis(); List tokens = new ArrayList<>(); for (int i = 0; i < numTokens; i++) { byte[] challenge = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); - tokens.add(HashCashService.mint(payload, challenge, difficulty).get()); + tokens.add(new HashCashService().mint(payload, challenge, difficulty).get()); } double size = tokens.size(); long ts2 = System.currentTimeMillis(); long averageCounter = Math.round(tokens.stream().mapToLong(ProofOfWork::getCounter).average().orElse(0)); - boolean allValid = tokens.stream().allMatch(HashCashService::verify); + boolean allValid = tokens.stream().allMatch(new HashCashService()::verify); assertTrue(allValid); double time1 = (System.currentTimeMillis() - ts) / size; double time2 = (System.currentTimeMillis() - ts2) / size; stringBuilder.append("\nMinting ").append(numTokens) - .append(" tokens with ").append(difficulty) + .append(" tokens with > ").append(log2Difficulty) .append(" leading zeros took ").append(time1) .append(" ms per token and ").append(averageCounter) .append(" iterations in average. Verification took ").append(time2) diff --git a/common/src/test/java/bisq/common/crypto/ProofOfWorkServiceTest.java b/common/src/test/java/bisq/common/crypto/ProofOfWorkServiceTest.java deleted file mode 100644 index a4bab784b99..00000000000 --- a/common/src/test/java/bisq/common/crypto/ProofOfWorkServiceTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package bisq.common.crypto; - -import org.apache.commons.lang3.RandomStringUtils; - -import java.nio.charset.StandardCharsets; - -import java.math.BigInteger; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.ExecutionException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.junit.Test; - -import static org.junit.Assert.assertTrue; - -public class ProofOfWorkServiceTest { - private final static Logger log = LoggerFactory.getLogger(ProofOfWorkServiceTest.class); - - // @Ignore - @Test - public void testDiffIncrease() throws ExecutionException, InterruptedException { - StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < 12; i++) { - run(i, stringBuilder); - } - log.info(stringBuilder.toString()); - - //Test result on a 4 GHz Intel Core i7: - //Minting 1000 tokens with 0 leading zeros took 0.279 ms per token and 2 iterations in average. Verification took 0.025 ms per token. - //Minting 1000 tokens with 1 leading zeros took 0.063 ms per token and 4 iterations in average. Verification took 0.007 ms per token. - //Minting 1000 tokens with 2 leading zeros took 0.074 ms per token and 8 iterations in average. Verification took 0.004 ms per token. - //Minting 1000 tokens with 3 leading zeros took 0.117 ms per token and 16 iterations in average. Verification took 0.003 ms per token. - //Minting 1000 tokens with 4 leading zeros took 0.116 ms per token and 33 iterations in average. Verification took 0.003 ms per token. - //Minting 1000 tokens with 5 leading zeros took 0.204 ms per token and 65 iterations in average. Verification took 0.003 ms per token. - //Minting 1000 tokens with 6 leading zeros took 0.23 ms per token and 131 iterations in average. Verification took 0.002 ms per token. - //Minting 1000 tokens with 7 leading zeros took 0.445 ms per token and 270 iterations in average. Verification took 0.002 ms per token. - //Minting 1000 tokens with 8 leading zeros took 0.856 ms per token and 530 iterations in average. Verification took 0.002 ms per token. - //Minting 1000 tokens with 9 leading zeros took 1.629 ms per token and 988 iterations in average. Verification took 0.002 ms per token. - //Minting 1000 tokens with 10 leading zeros took 3.291 ms per token and 2103 iterations in average. Verification took 0.002 ms per token. - //Minting 1000 tokens with 11 leading zeros took 6.259 ms per token and 4009 iterations in average. Verification took 0.001 ms per token. - //Minting 1000 tokens with 12 leading zeros took 13.845 ms per token and 8254 iterations in average. Verification took 0.002 ms per token. - //Minting 1000 tokens with 13 leading zeros took 26.052 ms per token and 16645 iterations in average. Verification took 0.002 ms per token. - - //Minting 100 tokens with 14 leading zeros took 69.14 ms per token and 40917 iterations in average. Verification took 0.06 ms per token. - //Minting 100 tokens with 15 leading zeros took 102.14 ms per token and 65735 iterations in average. Verification took 0.01 ms per token. - //Minting 100 tokens with 16 leading zeros took 209.44 ms per token and 135137 iterations in average. Verification took 0.01 ms per token. - //Minting 100 tokens with 17 leading zeros took 409.46 ms per token and 263751 iterations in average. Verification took 0.01 ms per token. - //Minting 100 tokens with 18 leading zeros took 864.21 ms per token and 555671 iterations in average. Verification took 0.0 ms per token. - //Minting 100 tokens with 19 leading zeros took 1851.33 ms per token and 1097760 iterations in average. Verification took 0.0 ms per token. - } - - private void run(int numLeadingZeros, StringBuilder stringBuilder) throws ExecutionException, InterruptedException { - int numTokens = 1000; - BigInteger target = ProofOfWorkService.getTarget(numLeadingZeros); - byte[] payload = RandomStringUtils.random(50, true, true).getBytes(StandardCharsets.UTF_8); - long ts = System.currentTimeMillis(); - List tokens = new ArrayList<>(); - for (int i = 0; i < numTokens; i++) { - byte[] challenge = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8); - tokens.add(ProofOfWorkService.mint(payload, challenge, target).get()); - } - double size = tokens.size(); - long ts2 = System.currentTimeMillis(); - long averageCounter = Math.round(tokens.stream().mapToLong(ProofOfWork::getCounter).average().orElse(0)); - boolean allValid = tokens.stream().allMatch(ProofOfWorkService::verify); - assertTrue(allValid); - double time1 = (System.currentTimeMillis() - ts) / size; - double time2 = (System.currentTimeMillis() - ts2) / size; - stringBuilder.append("\nMinting ").append(numTokens) - .append(" tokens with ").append(numLeadingZeros) - .append(" leading zeros took ").append(time1) - .append(" ms per token and ").append(averageCounter) - .append(" iterations in average. Verification took ").append(time2) - .append(" ms per token."); - } -} diff --git a/common/src/test/java/bisq/common/util/PermutationTest.java b/common/src/test/java/bisq/common/util/PermutationTest.java index dc3c65c5130..c539992d2a0 100644 --- a/common/src/test/java/bisq/common/util/PermutationTest.java +++ b/common/src/test/java/bisq/common/util/PermutationTest.java @@ -21,8 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.function.BiFunction; - +import java.util.function.BiPredicate; import org.junit.Test; @@ -108,7 +107,7 @@ public void testFindMatchingPermutation() { List result; List list; List expected; - BiFunction, Boolean> predicate = (target, variationList) -> variationList.toString().equals(target); + BiPredicate> predicate = (target, variationList) -> variationList.toString().equals(target); list = Arrays.asList(a, b, c, d, e); @@ -124,11 +123,11 @@ public void testFindMatchingPermutation() { @Test public void testBreakAtLimit() { - BiFunction, Boolean> predicate = + BiPredicate> predicate = (target, variationList) -> variationList.toString().equals(target); var list = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o"); var expected = Arrays.asList("b", "g", "m"); - + // Takes around 32508 tries starting from longer strings var limit = 100000; var result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index ddf402d323d..0f85803712d 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -77,7 +77,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.function.Function; import java.util.stream.Collectors; @@ -488,7 +488,7 @@ private Optional> findPermutatedListMatchingMajority(byte[] majo List list = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); long ts = System.currentTimeMillis(); - BiFunction, Boolean> predicate = (hash, variation) -> + BiPredicate> predicate = (hash, variation) -> isListMatchingMajority(hash, variation, false); List result = PermutationUtil.findMatchingPermutation(majorityVoteListHash, list, predicate, 1000000); diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index 3af0b8c832e..70f088eb9b7 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -105,9 +105,12 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { // added at BsqSwap release private final boolean disablePowMessage; - // Number of leading zeros for pow for BSQ swap offers. Difficulty of 8 requires 0.856 ms in average, 15 about 100 ms. - // See ProofOfWorkTest for more info. - private final int powDifficulty; + // 2 ** effective-number-of-leading-zeros for pow for BSQ swap offers, when using Hashcash (= version 0), and + // a similar difficulty for Equihash (= versions 1) or later schemes. Difficulty of 2 ** 8 (= 256) requires + // 0.856 ms in average, 2 ** 15 (= 32768) about 100 ms. See HashCashServiceTest for more info. + private final double powDifficulty; + // Enabled PoW version numbers in reverse order of preference, starting with 0 for Hashcash. + private final List enabledPowVersions; // Added at v 1.8.0 // BSQ fee gets updated in proposals repo (e.g. https://github.com/bisq-network/proposals/issues/345) @@ -148,6 +151,7 @@ static Filter cloneWithSig(Filter filter, String signatureAsBase64) { filter.isDisableApi(), filter.isDisablePowMessage(), filter.getPowDifficulty(), + filter.getEnabledPowVersions(), filter.getMakerFeeBtc(), filter.getTakerFeeBtc(), filter.getMakerFeeBsq(), @@ -186,6 +190,7 @@ static Filter cloneWithoutSig(Filter filter) { filter.isDisableApi(), filter.isDisablePowMessage(), filter.getPowDifficulty(), + filter.getEnabledPowVersions(), filter.getMakerFeeBtc(), filter.getTakerFeeBtc(), filter.getMakerFeeBsq(), @@ -218,7 +223,8 @@ public Filter(List bannedOfferIds, boolean disableMempoolValidation, boolean disableApi, boolean disablePowMessage, - int powDifficulty, + double powDifficulty, + List enabledPowVersions, long makerFeeBtc, long takerFeeBtc, long makerFeeBsq, @@ -253,6 +259,7 @@ public Filter(List bannedOfferIds, disableApi, disablePowMessage, powDifficulty, + enabledPowVersions, makerFeeBtc, takerFeeBtc, makerFeeBsq, @@ -294,7 +301,8 @@ public Filter(List bannedOfferIds, boolean disableMempoolValidation, boolean disableApi, boolean disablePowMessage, - int powDifficulty, + double powDifficulty, + List enabledPowVersions, long makerFeeBtc, long takerFeeBtc, long makerFeeBsq, @@ -329,6 +337,7 @@ public Filter(List bannedOfferIds, this.disableApi = disableApi; this.disablePowMessage = disablePowMessage; this.powDifficulty = powDifficulty; + this.enabledPowVersions = enabledPowVersions; this.makerFeeBtc = makerFeeBtc; this.takerFeeBtc = takerFeeBtc; this.makerFeeBsq = makerFeeBsq; @@ -376,6 +385,7 @@ public protobuf.StoragePayload toProtoMessage() { .setDisableApi(disableApi) .setDisablePowMessage(disablePowMessage) .setPowDifficulty(powDifficulty) + .addAllEnabledPowVersions(enabledPowVersions) .setMakerFeeBtc(makerFeeBtc) .setTakerFeeBtc(takerFeeBtc) .setMakerFeeBsq(makerFeeBsq) @@ -422,6 +432,7 @@ public static Filter fromProto(protobuf.Filter proto) { proto.getDisableApi(), proto.getDisablePowMessage(), proto.getPowDifficulty(), + proto.getEnabledPowVersionsList(), proto.getMakerFeeBtc(), proto.getTakerFeeBtc(), proto.getMakerFeeBsq(), @@ -473,6 +484,7 @@ public String toString() { ",\n disableApi=" + disableApi + ",\n disablePowMessage=" + disablePowMessage + ",\n powDifficulty=" + powDifficulty + + ",\n enabledPowVersions=" + enabledPowVersions + ",\n makerFeeBtc=" + makerFeeBtc + ",\n takerFeeBtc=" + takerFeeBtc + ",\n makerFeeBsq=" + makerFeeBsq + diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index dfae9cf6858..2e57d702b29 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -37,8 +37,9 @@ import bisq.common.app.Version; import bisq.common.config.Config; import bisq.common.config.ConfigFileEditor; -import bisq.common.crypto.HashCashService; import bisq.common.crypto.KeyRing; +import bisq.common.crypto.ProofOfWork; +import bisq.common.crypto.ProofOfWorkService; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Sha256Hash; @@ -57,7 +58,6 @@ import java.math.BigInteger; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -65,7 +65,6 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.lang.reflect.Method; @@ -87,12 +86,6 @@ public class FilterManager { private static final String BANNED_SEED_NODES = "bannedSeedNodes"; private static final String BANNED_BTC_NODES = "bannedBtcNodes"; - private final BiFunction challengeValidation = Arrays::equals; - // We only require a new pow if difficulty has increased - private final BiFunction difficultyValidation = - (value, controlValue) -> value - controlValue >= 0; - - /////////////////////////////////////////////////////////////////////////////////////////// // Listener /////////////////////////////////////////////////////////////////////////////////////////// @@ -144,7 +137,7 @@ public FilterManager(P2PService p2PService, "029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f", "034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9"); - networkFilter.setBannedNodeFunction(this::isNodeAddressBannedFromNetwork); + networkFilter.setBannedNodePredicate(this::isNodeAddressBannedFromNetwork); } @@ -491,13 +484,16 @@ public boolean isProofOfWorkValid(Offer offer) { if (filter == null) { return true; } - checkArgument(offer.getBsqSwapOfferPayload().isPresent(), - "Offer payload must be BsqSwapOfferPayload"); - return HashCashService.verify(offer.getBsqSwapOfferPayload().get().getProofOfWork(), - HashCashService.getBytes(offer.getId() + offer.getOwnerNodeAddress().toString()), - filter.getPowDifficulty(), - challengeValidation, - difficultyValidation); + checkArgument(offer.getBsqSwapOfferPayload().isPresent(), "Offer payload must be BsqSwapOfferPayload"); + ProofOfWork pow = offer.getBsqSwapOfferPayload().get().getProofOfWork(); + var service = ProofOfWorkService.forVersion(pow.getVersion()); + return service.isPresent() && getEnabledPowVersions().contains(pow.getVersion()) && + service.get().verify(pow, offer.getId(), offer.getOwnerNodeAddress().toString(), filter.getPowDifficulty()); + } + + public List getEnabledPowVersions() { + Filter filter = getFilter(); + return filter != null && !filter.getEnabledPowVersions().isEmpty() ? filter.getEnabledPowVersions() : List.of(0); } diff --git a/core/src/main/java/bisq/core/network/CoreNetworkFilter.java b/core/src/main/java/bisq/core/network/CoreNetworkFilter.java index b261d421537..536ea82287a 100644 --- a/core/src/main/java/bisq/core/network/CoreNetworkFilter.java +++ b/core/src/main/java/bisq/core/network/CoreNetworkFilter.java @@ -28,14 +28,14 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Function; +import java.util.function.Predicate; import lombok.extern.slf4j.Slf4j; @Slf4j public class CoreNetworkFilter implements NetworkFilter { private final Set bannedPeersFromOptions = new HashSet<>(); - private Function bannedNodeFunction; + private Predicate bannedNodePredicate; /** * @param banList List of banned peers from program argument @@ -46,13 +46,13 @@ public CoreNetworkFilter(@Named(Config.BAN_LIST) List banList) { } @Override - public void setBannedNodeFunction(Function bannedNodeFunction) { - this.bannedNodeFunction = bannedNodeFunction; + public void setBannedNodePredicate(Predicate bannedNodePredicate) { + this.bannedNodePredicate = bannedNodePredicate; } @Override public boolean isPeerBanned(NodeAddress nodeAddress) { return bannedPeersFromOptions.contains(nodeAddress) || - bannedNodeFunction != null && bannedNodeFunction.apply(nodeAddress); + bannedNodePredicate != null && bannedNodePredicate.test(nodeAddress); } } diff --git a/core/src/main/java/bisq/core/offer/bsq_swap/OpenBsqSwapOfferService.java b/core/src/main/java/bisq/core/offer/bsq_swap/OpenBsqSwapOfferService.java index b55a1b70e44..70735a57064 100644 --- a/core/src/main/java/bisq/core/offer/bsq_swap/OpenBsqSwapOfferService.java +++ b/core/src/main/java/bisq/core/offer/bsq_swap/OpenBsqSwapOfferService.java @@ -43,7 +43,7 @@ import bisq.common.UserThread; import bisq.common.app.Version; -import bisq.common.crypto.HashCashService; +import bisq.common.crypto.ProofOfWorkService; import bisq.common.crypto.PubKeyRing; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; @@ -61,6 +61,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -195,13 +196,11 @@ public void requestNewOffer(String offerId, amount.value, minAmount.value); - NodeAddress makerAddress = p2PService.getAddress(); + NodeAddress makerAddress = Objects.requireNonNull(p2PService.getAddress()); offerUtil.validateBasicOfferData(PaymentMethod.BSQ_SWAP, "BSQ"); - byte[] payload = HashCashService.getBytes(offerId); - byte[] challenge = HashCashService.getBytes(offerId + Objects.requireNonNull(makerAddress)); - int difficulty = getPowDifficulty(); - HashCashService.mint(payload, challenge, difficulty) + double difficulty = getPowDifficulty(); + getPowService().mint(offerId, makerAddress.getFullAddress(), difficulty) .whenComplete((proofOfWork, throwable) -> { // We got called from a non user thread... UserThread.execute(() -> { @@ -247,7 +246,7 @@ public void activateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (isProofOfWorkInvalid(openOffer.getOffer())) { - redoProofOrWorkAndRepublish(openOffer); + redoProofOfWorkAndRepublish(openOffer); return; } @@ -265,7 +264,7 @@ void requestPersistence() { void enableBsqSwapOffer(OpenOffer openOffer) { if (isProofOfWorkInvalid(openOffer.getOffer())) { - redoProofOrWorkAndRepublish(openOffer); + redoProofOfWorkAndRepublish(openOffer); return; } @@ -297,7 +296,7 @@ private void onOpenOffersAdded(List list) { .forEach(openOffer -> { if (isProofOfWorkInvalid(openOffer.getOffer())) { // Avoiding ConcurrentModificationException - UserThread.execute(() -> redoProofOrWorkAndRepublish(openOffer)); + UserThread.execute(() -> redoProofOfWorkAndRepublish(openOffer)); } else { OpenBsqSwapOffer openBsqSwapOffer = new OpenBsqSwapOffer(openOffer, this, @@ -333,7 +332,7 @@ private void onProofOfWorkDifficultyChanged() { .filter(openBsqSwapOffer -> isProofOfWorkInvalid(openBsqSwapOffer.getOffer())) .forEach(openBsqSwapOffer -> { // Avoiding ConcurrentModificationException - UserThread.execute(() -> redoProofOrWorkAndRepublish(openBsqSwapOffer.getOpenOffer())); + UserThread.execute(() -> redoProofOfWorkAndRepublish(openBsqSwapOffer.getOpenOffer())); }); } @@ -342,16 +341,14 @@ private void onProofOfWorkDifficultyChanged() { // Proof of work /////////////////////////////////////////////////////////////////////////////////////////// - private void redoProofOrWorkAndRepublish(OpenOffer openOffer) { - // This triggers our onOpenOffersRemoved handler so we dont handle removal here + private void redoProofOfWorkAndRepublish(OpenOffer openOffer) { + // This triggers our onOpenOffersRemoved handler so we don't handle removal here openOfferManager.removeOpenOffer(openOffer); String newOfferId = OfferUtil.getOfferIdWithMutationCounter(openOffer.getId()); - byte[] payload = HashCashService.getBytes(newOfferId); NodeAddress nodeAddress = Objects.requireNonNull(openOffer.getOffer().getMakerNodeAddress()); - byte[] challenge = HashCashService.getBytes(newOfferId + nodeAddress); - int difficulty = getPowDifficulty(); - HashCashService.mint(payload, challenge, difficulty) + double difficulty = getPowDifficulty(); + getPowService().mint(newOfferId, nodeAddress.getFullAddress(), difficulty) .whenComplete((proofOfWork, throwable) -> { // We got called from a non user thread... UserThread.execute(() -> { @@ -374,7 +371,7 @@ private void redoProofOrWorkAndRepublish(OpenOffer openOffer) { if (!newOpenOffer.isDeactivated()) { openOfferManager.maybeRepublishOffer(newOpenOffer); } - // This triggers our onOpenOffersAdded handler so we dont handle adding to our list here + // This triggers our onOpenOffersAdded handler so we don't handle adding to our list here openOfferManager.addOpenBsqSwapOffer(newOpenOffer); }); }); @@ -390,7 +387,19 @@ private boolean isProofOfWorkInvalid(Offer offer) { } - private int getPowDifficulty() { - return filterManager.getFilter() != null ? filterManager.getFilter().getPowDifficulty() : 0; + private double getPowDifficulty() { + return filterManager.getFilter() != null ? filterManager.getFilter().getPowDifficulty() : 0.0; + } + + private ProofOfWorkService getPowService() { + var service = filterManager.getEnabledPowVersions().stream() + .flatMap(v -> ProofOfWorkService.forVersion(v).stream()) + .findFirst(); + if (!service.isPresent()) { + // We cannot exit normally, else we get caught in an infinite loop generating invalid PoWs. + throw new NoSuchElementException("Could not find a suitable PoW version to use."); + } + log.info("Selected PoW version {}, service instance {}", service.get().getVersion(), service.get()); + return service.get(); } } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccounts.java b/core/src/main/java/bisq/core/payment/PaymentAccounts.java index 6f871ce20f9..578dfee7b4b 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccounts.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccounts.java @@ -25,7 +25,7 @@ import java.util.Date; import java.util.List; import java.util.Set; -import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -38,14 +38,14 @@ class PaymentAccounts { private final Set accounts; private final AccountAgeWitnessService accountAgeWitnessService; - private final BiFunction validator; + private final BiPredicate validator; PaymentAccounts(Set accounts, AccountAgeWitnessService accountAgeWitnessService) { this(accounts, accountAgeWitnessService, PaymentAccountUtil::isPaymentAccountValidForOffer); } PaymentAccounts(Set accounts, AccountAgeWitnessService accountAgeWitnessService, - BiFunction validator) { + BiPredicate validator) { this.accounts = accounts; this.accountAgeWitnessService = accountAgeWitnessService; this.validator = validator; @@ -63,7 +63,7 @@ PaymentAccount getOldestPaymentAccountForOffer(Offer offer) { private List sortValidAccounts(Offer offer) { Comparator comparator = this::compareByTradeLimit; return accounts.stream() - .filter(account -> validator.apply(offer, account)) + .filter(account -> validator.test(offer, account)) .sorted(comparator.reversed()) .collect(Collectors.toList()); } diff --git a/core/src/main/java/bisq/core/trade/bisq_v1/FailedTradesManager.java b/core/src/main/java/bisq/core/trade/bisq_v1/FailedTradesManager.java index 295342511a6..412bacb03df 100644 --- a/core/src/main/java/bisq/core/trade/bisq_v1/FailedTradesManager.java +++ b/core/src/main/java/bisq/core/trade/bisq_v1/FailedTradesManager.java @@ -33,7 +33,7 @@ import javafx.collections.ObservableList; import java.util.Optional; -import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Stream; import org.slf4j.Logger; @@ -52,7 +52,7 @@ public class FailedTradesManager implements PersistedDataHost { private final TradeUtil tradeUtil; private final DumpDelayedPayoutTx dumpDelayedPayoutTx; @Setter - private Function unFailTradeCallback; + private Predicate unFailTradeCallback; @Inject public FailedTradesManager(KeyRing keyRing, @@ -123,7 +123,7 @@ public void unFailTrade(Trade trade) { if (unFailTradeCallback == null) return; - if (unFailTradeCallback.apply(trade)) { + if (unFailTradeCallback.test(trade)) { log.info("Unfailing trade {}", trade.getId()); if (failedTrades.remove(trade)) { requestPersistence(); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 1cc275d92bf..89ea1742bc9 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2782,6 +2782,7 @@ filterWindow.disableApi=Disable API filterWindow.disableMempoolValidation=Disable Mempool Validation filterWindow.disablePowMessage=Disable messages requiring Proof of Work filterWindow.powDifficulty=Proof of work difficulty (BSQ swap offers) +filterWindow.enabledPowVersions=Enabled proof of work versions (comma sep. integers) filterWindow.makerFeeBtc=Min. BTC maker fee (e.g. 0.001) filterWindow.takerFeeBtc=Min. BTC taker fee (e.g. 0.007) filterWindow.makerFeeBsq=Min. BSQ maker fee (e.g. 15.14) diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java index 3c953583027..42509b95f2d 100644 --- a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -72,6 +72,7 @@ public void testRoundtripFull() { false, false, 0, + Lists.newArrayList(), 0, 0, 0, diff --git a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java index 2dcbd5b1a19..8ddf70cb153 100644 --- a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java +++ b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java @@ -133,6 +133,7 @@ private static Filter filterWithReceivers(List btcFeeReceiverAddresses) false, false, 0, + Lists.newArrayList(), 0, 0, 0, diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index d393de487df..658e84acd33 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -41,7 +41,8 @@ import javax.inject.Named; -import org.apache.commons.lang3.StringUtils; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; import javafx.scene.Scene; import javafx.scene.control.Button; @@ -55,9 +56,6 @@ import javafx.geometry.HPos; import javafx.geometry.Insets; -import javafx.collections.FXCollections; - -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -189,7 +187,9 @@ private void addContent() { Res.get("filterWindow.disablePowMessage")); InputTextField powDifficultyTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.powDifficulty")); - powDifficultyTF.setText("0"); + powDifficultyTF.setText("0.0"); + InputTextField enabledPowVersionsTF = addInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.enabledPowVersions")); InputTextField makerFeeBtcTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.makerFeeBtc")); InputTextField takerFeeBtcTF = addInputTextField(gridPane, ++rowIndex, @@ -217,6 +217,7 @@ private void addContent() { setupFieldFromList(btcNodesTF, filter.getBtcNodes()); setupFieldFromList(bannedPrivilegedDevPubKeysTF, filter.getBannedPrivilegedDevPubKeys()); setupFieldFromList(autoConfExplorersTF, filter.getBannedAutoConfExplorers()); + setupFieldFromList(enabledPowVersionsTF, filter.getEnabledPowVersions()); preventPublicBtcNetworkCheckBox.setSelected(filter.isPreventPublicBtcNetwork()); disableDaoCheckBox.setSelected(filter.isDisableDao()); @@ -269,7 +270,8 @@ private void addContent() { disableMempoolValidationCheckBox.isSelected(), disableApiCheckBox.isSelected(), disablePowMessage.isSelected(), - Integer.parseInt(powDifficultyTF.getText()), + Double.parseDouble(powDifficultyTF.getText()), + readAsList(enabledPowVersionsTF).stream().map(Integer::parseInt).collect(Collectors.toList()), ParsingUtils.parseToCoin(makerFeeBtcTF.getText(), btcFormatter).value, ParsingUtils.parseToCoin(takerFeeBtcTF.getText(), btcFormatter).value, ParsingUtils.parseToCoin(makerFeeBsqTF.getText(), bsqFormatter).value, @@ -325,9 +327,10 @@ private void addDevFilter(Button removeFilterMessageButton, String privKeyString hide(); } - private void setupFieldFromList(InputTextField field, Collection values) { - if (values != null) - field.setText(String.join(", ", values)); + private void setupFieldFromList(InputTextField field, Collection values) { + if (values != null) { + field.setText(Joiner.on(", ").join(values)); + } } private void setupFieldFromPaymentAccountFiltersList(InputTextField field, List values) { @@ -349,11 +352,7 @@ private void setupFieldFromPaymentAccountFiltersList(InputTextField field, List< } private List readAsList(InputTextField field) { - if (field.getText().isEmpty()) { - return FXCollections.emptyObservableList(); - } else { - return Arrays.asList(StringUtils.deleteWhitespace(field.getText()).split(",")); - } + return Splitter.on(',').trimResults().omitEmptyStrings().splitToList(field.getText()); } private List readAsPaymentAccountFiltersList(InputTextField field) { diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkFilter.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkFilter.java index 3dcf040e21f..15940c35326 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkFilter.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkFilter.java @@ -19,10 +19,10 @@ import bisq.network.p2p.NodeAddress; -import java.util.function.Function; +import java.util.function.Predicate; public interface NetworkFilter { boolean isPeerBanned(NodeAddress nodeAddress); - void setBannedNodeFunction(Function isNodeAddressBanned); + void setBannedNodePredicate(Predicate isNodeAddressBanned); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 0382145fd5f..7f9b4d435fc 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -85,8 +85,8 @@ message NetworkEnvelope { ShareBuyerPaymentAccountMessage share_buyer_payment_account_message = 54; // Added at 1.7.0 SellersBsqSwapRequest sellers_bsq_swap_request = 55; - BuyersBsqSwapRequest buyers_bsq_swap_request= 56; - BsqSwapTxInputsMessage bsq_swap_tx_inputs_message= 57; + BuyersBsqSwapRequest buyers_bsq_swap_request = 56; + BsqSwapTxInputsMessage bsq_swap_tx_inputs_message = 57; BsqSwapFinalizeTxRequest bsq_swap_finalize_tx_request = 58; BsqSwapFinalizedTxMessage bsq_swap_finalized_tx_message = 59; } @@ -769,11 +769,12 @@ message Filter { bool disable_api = 27; bool disable_mempool_validation = 28; bool disable_pow_message = 29; - int32 pow_difficulty = 30; + double pow_difficulty = 30; int64 maker_fee_btc = 31; int64 taker_fee_btc = 32; int64 maker_fee_bsq = 33; int64 taker_fee_bsq = 34; + repeated int32 enabled_pow_versions = 35; } // Deprecated @@ -881,8 +882,10 @@ message ProofOfWork { bytes payload = 1; int64 counter = 2; bytes challenge = 3; - bytes difficulty = 4; + double difficulty = 4; int64 duration = 5; + bytes solution = 6; + int32 version = 7; } message AccountAgeWitness { @@ -2036,7 +2039,7 @@ message BaseBlock { } message BsqBlockStore { - repeated BaseBlock blocks = 1; + repeated BaseBlock blocks = 1; } message RawBlock {