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 0000000000..25da45688e
--- /dev/null
+++ b/common/src/main/java/bisq/common/crypto/EquihashProofOfWorkService.java
@@ -0,0 +1,76 @@
+/*
+ * 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.Longs;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+
+import lombok.extern.slf4j.Slf4j;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+@Slf4j
+public class EquihashProofOfWorkService extends ProofOfWorkService {
+ /** Rough cost of one Hashcash iteration 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(String itemId, byte[] challenge, int log2Difficulty) {
+ double difficulty = adjustedDifficulty(log2Difficulty);
+ log.info("Got adjusted difficulty: {}", difficulty);
+
+ return CompletableFuture.supplyAsync(() -> {
+ long ts = System.currentTimeMillis();
+ byte[] solution = new Equihash(90, 5, difficulty).puzzle(challenge).findSolution().serialize();
+ long counter = Longs.fromByteArray(Arrays.copyOf(solution, 8));
+ var proofOfWork = new ProofOfWork(solution, counter, challenge, log2Difficulty,
+ System.currentTimeMillis() - ts, getVersion());
+ log.info("Completed minting proofOfWork: {}", proofOfWork);
+ return proofOfWork;
+ });
+ }
+
+ @Override
+ public byte[] getChallenge(String itemId, String ownerId) {
+ checkArgument(!StringUtils.contains(itemId, '\0'));
+ checkArgument(!StringUtils.contains(ownerId, '\0'));
+ return Hash.getSha256Hash(checkNotNull(itemId) + "\0" + checkNotNull(ownerId));
+ }
+
+ @Override
+ boolean verify(ProofOfWork proofOfWork) {
+ double difficulty = adjustedDifficulty(proofOfWork.getNumLeadingZeros());
+
+ var puzzle = new Equihash(90, 5, difficulty).puzzle(proofOfWork.getChallenge());
+ return puzzle.deserializeSolution(proofOfWork.getPayload()).verify();
+ }
+
+ private static double adjustedDifficulty(int log2Difficulty) {
+ return Equihash.adjustDifficulty(Math.scalb(DIFFICULTY_SCALE_FACTOR, log2Difficulty),
+ Equihash.EQUIHASH_n_5_MEAN_SOLUTION_COUNT_PER_NONCE);
+ }
+}
diff --git a/common/src/main/java/bisq/common/crypto/HashCashService.java b/common/src/main/java/bisq/common/crypto/HashCashService.java
index ed0e30f0c5..bebbdd8246 100644
--- a/common/src/main/java/bisq/common/crypto/HashCashService.java
+++ b/common/src/main/java/bisq/common/crypto/HashCashService.java
@@ -35,40 +35,56 @@ import lombok.extern.slf4j.Slf4j;
* See https://www.hashcash.org/papers/hashcash.pdf
*/
@Slf4j
-public class HashCashService {
+public class HashCashService extends ProofOfWorkService {
// Default validations. Custom implementations might use tolerance.
private static final BiPredicate isChallengeValid = Arrays::equals;
private static final BiPredicate isDifficultyValid = Integer::equals;
- public static CompletableFuture mint(byte[] payload,
- byte[] challenge,
- int difficulty) {
+ HashCashService() {
+ super(0);
+ }
+
+ @Override
+ public CompletableFuture mint(String itemId, byte[] challenge, int log2Difficulty) {
+ byte[] payload = getBytes(itemId);
+ return mint(payload, challenge, log2Difficulty);
+ }
+
+ @Override
+ public byte[] getChallenge(String itemId, String ownerId) {
+ return getBytes(itemId + ownerId);
+ }
+
+ static CompletableFuture mint(byte[] payload,
+ byte[] challenge,
+ int difficulty) {
return HashCashService.mint(payload,
challenge,
difficulty,
HashCashService::testDifficulty);
}
- public static boolean verify(ProofOfWork proofOfWork) {
+ @Override
+ boolean verify(ProofOfWork proofOfWork) {
return verify(proofOfWork,
proofOfWork.getChallenge(),
proofOfWork.getNumLeadingZeros());
}
- public static boolean verify(ProofOfWork proofOfWork,
- byte[] controlChallenge,
- int controlDifficulty) {
+ static boolean verify(ProofOfWork proofOfWork,
+ byte[] controlChallenge,
+ int controlDifficulty) {
return HashCashService.verify(proofOfWork,
controlChallenge,
controlDifficulty,
HashCashService::testDifficulty);
}
- public static boolean verify(ProofOfWork proofOfWork,
- byte[] controlChallenge,
- int controlDifficulty,
- BiPredicate challengeValidation,
- BiPredicate difficultyValidation) {
+ static boolean verify(ProofOfWork proofOfWork,
+ byte[] controlChallenge,
+ int controlDifficulty,
+ BiPredicate challengeValidation,
+ BiPredicate difficultyValidation) {
return HashCashService.verify(proofOfWork,
controlChallenge,
controlDifficulty,
@@ -139,7 +155,7 @@ public class HashCashService {
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
- public static byte[] getBytes(String value) {
+ private static byte[] getBytes(String value) {
return value.getBytes(StandardCharsets.UTF_8);
}
diff --git a/common/src/main/java/bisq/common/crypto/ProofOfWorkService.java b/common/src/main/java/bisq/common/crypto/ProofOfWorkService.java
new file mode 100644
index 0000000000..419c6587ef
--- /dev/null
+++ b/common/src/main/java/bisq/common/crypto/ProofOfWorkService.java
@@ -0,0 +1,72 @@
+/*
+ * 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.base.Preconditions;
+
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.BiPredicate;
+
+import lombok.Getter;
+
+public abstract class ProofOfWorkService {
+ private static class InstanceHolder {
+ private static final ProofOfWorkService[] INSTANCES = {
+ new HashCashService(),
+ new EquihashProofOfWorkService(1)
+ };
+ }
+
+ public static Optional forVersion(int version) {
+ return version >= 0 && version < InstanceHolder.INSTANCES.length ?
+ Optional.of(InstanceHolder.INSTANCES[version]) : Optional.empty();
+ }
+
+ @Getter
+ private final int version;
+
+ ProofOfWorkService(int version) {
+ this.version = version;
+ }
+
+ public abstract CompletableFuture mint(String itemId, byte[] challenge, int log2Difficulty);
+
+ public abstract byte[] getChallenge(String itemId, String ownerId);
+
+ abstract boolean verify(ProofOfWork proofOfWork);
+
+ public CompletableFuture mint(String itemId, String ownerId, int log2Difficulty) {
+ return mint(itemId, getChallenge(itemId, ownerId), log2Difficulty);
+ }
+
+ public boolean verify(ProofOfWork proofOfWork,
+ String itemId,
+ String ownerId,
+ int controlLog2Difficulty,
+ BiPredicate challengeValidation,
+ BiPredicate difficultyValidation) {
+
+ Preconditions.checkArgument(proofOfWork.getVersion() == version);
+
+ byte[] controlChallenge = getChallenge(itemId, ownerId);
+ return challengeValidation.test(proofOfWork.getChallenge(), controlChallenge) &&
+ difficultyValidation.test(proofOfWork.getNumLeadingZeros(), controlLog2Difficulty) &&
+ verify(proofOfWork);
+ }
+}
diff --git a/common/src/test/java/bisq/common/crypto/HashCashServiceTest.java b/common/src/test/java/bisq/common/crypto/HashCashServiceTest.java
index 115483d25f..9c4fcb1f53 100644
--- a/common/src/test/java/bisq/common/crypto/HashCashServiceTest.java
+++ b/common/src/test/java/bisq/common/crypto/HashCashServiceTest.java
@@ -36,7 +36,7 @@ public class HashCashServiceTest {
@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());
@@ -70,7 +70,7 @@ public class HashCashServiceTest {
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;
diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java
index 5d80591b39..c95c605192 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.DevEnv;
import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.config.ConfigFileEditor;
-import bisq.common.crypto.HashCashService;
+import bisq.common.crypto.ProofOfWorkService;
import bisq.common.crypto.KeyRing;
+import bisq.common.crypto.ProofOfWork;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
@@ -498,13 +499,23 @@ public class FilterManager {
}
checkArgument(offer.getBsqSwapOfferPayload().isPresent(),
"Offer payload must be BsqSwapOfferPayload");
- return HashCashService.verify(offer.getBsqSwapOfferPayload().get().getProofOfWork(),
- HashCashService.getBytes(offer.getId() + offer.getOwnerNodeAddress().toString()),
+ ProofOfWork pow = offer.getBsqSwapOfferPayload().get().getProofOfWork();
+ var service = ProofOfWorkService.forVersion(pow.getVersion());
+ if (!service.isPresent() || !getEnabledPowVersions().contains(pow.getVersion())) {
+ return false;
+ }
+ return service.get().verify(offer.getBsqSwapOfferPayload().get().getProofOfWork(),
+ offer.getId(), offer.getOwnerNodeAddress().toString(),
filter.getPowDifficulty(),
challengeValidation,
difficultyValidation);
}
+ public List getEnabledPowVersions() {
+ Filter filter = getFilter();
+ return filter != null && !filter.getEnabledPowVersions().isEmpty() ? filter.getEnabledPowVersions() : List.of(0);
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////
// Private
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 3f202abc30..962983b820 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.network.p2p.P2PService;
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.Date;
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 class OpenBsqSwapOfferService {
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)
+ int log2Difficulty = getPowDifficulty();
+ getPowService().mint(offerId, makerAddress.getFullAddress(), log2Difficulty)
.whenComplete((proofOfWork, throwable) -> {
// We got called from a non user thread...
UserThread.execute(() -> {
@@ -347,11 +346,9 @@ public class OpenBsqSwapOfferService {
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)
+ int log2Difficulty = getPowDifficulty();
+ getPowService().mint(newOfferId, nodeAddress.getFullAddress(), log2Difficulty)
.whenComplete((proofOfWork, throwable) -> {
// We got called from a non user thread...
UserThread.execute(() -> {
@@ -393,4 +390,16 @@ public class OpenBsqSwapOfferService {
private int getPowDifficulty() {
return filterManager.getFilter() != null ? filterManager.getFilter().getPowDifficulty() : 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();
+ }
}