Skip to content

Commit

Permalink
Add+enable service to mint/verify Equihash proofs of work
Browse files Browse the repository at this point in the history
Add an abstract base class, 'ProofOfWorkService', for the existing PoW
implementation 'HashCashService' and a new 'EquihashProofOfWorkService'
PoW implementation based on Equihash-90-5 (which has 72 byte solutions &
5-10 MB peak memory usage). Since the current 'ProofOfWork' protobuf
object only provides a 64-bit counter field to hold the puzzle solution
(as that is all Hashcash requires), repurpose the 'payload' field to
hold the Equihash puzzle solution bytes, with the 'challenge' field
equal to the puzzle seed: the SHA256 hash of the offerId & makerAddress.

Use a difficulty scale factor of 3e-5 (derived from benchmarking) to try
to make the average Hashcash & Equihash puzzle solution times roughly
equal for any given log-difficulty/numLeadingZeros integer chosen in the
filter.

NOTE: An empty enabled-version-list in the filter defaults to Hashcash
(= version 0) only. The new Equihash-90-5 PoW scheme is version 1.
  • Loading branch information
stejbac committed Nov 25, 2021
1 parent 647fc86 commit 3a25b7c
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<ProofOfWork> 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);
}
}
44 changes: 30 additions & 14 deletions common/src/main/java/bisq/common/crypto/HashCashService.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,40 +35,56 @@
* 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<byte[], byte[]> isChallengeValid = Arrays::equals;
private static final BiPredicate<Integer, Integer> isDifficultyValid = Integer::equals;

public static CompletableFuture<ProofOfWork> mint(byte[] payload,
byte[] challenge,
int difficulty) {
HashCashService() {
super(0);
}

@Override
public CompletableFuture<ProofOfWork> 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<ProofOfWork> 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<byte[], byte[]> challengeValidation,
BiPredicate<Integer, Integer> difficultyValidation) {
static boolean verify(ProofOfWork proofOfWork,
byte[] controlChallenge,
int controlDifficulty,
BiPredicate<byte[], byte[]> challengeValidation,
BiPredicate<Integer, Integer> difficultyValidation) {
return HashCashService.verify(proofOfWork,
controlChallenge,
controlDifficulty,
Expand Down Expand Up @@ -139,7 +155,7 @@ private static boolean verify(ProofOfWork proofOfWork, BiPredicate<byte[], Integ
// Utils
///////////////////////////////////////////////////////////////////////////////////////////

public static byte[] getBytes(String value) {
private static byte[] getBytes(String value) {
return value.getBytes(StandardCharsets.UTF_8);
}

Expand Down
72 changes: 72 additions & 0 deletions common/src/main/java/bisq/common/crypto/ProofOfWorkService.java
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<ProofOfWorkService> 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<ProofOfWork> mint(String itemId, byte[] challenge, int log2Difficulty);

public abstract byte[] getChallenge(String itemId, String ownerId);

abstract boolean verify(ProofOfWork proofOfWork);

public CompletableFuture<ProofOfWork> 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<byte[], byte[]> challengeValidation,
BiPredicate<Integer, Integer> difficultyValidation) {

Preconditions.checkArgument(proofOfWork.getVersion() == version);

byte[] controlChallenge = getChallenge(itemId, ownerId);
return challengeValidation.test(proofOfWork.getChallenge(), controlChallenge) &&
difficultyValidation.test(proofOfWork.getNumLeadingZeros(), controlLog2Difficulty) &&
verify(proofOfWork);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void testNumberOfLeadingZeros() {
@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());
Expand Down Expand Up @@ -70,7 +70,7 @@ private void run(int difficulty, StringBuilder stringBuilder) throws ExecutionEx
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;
Expand Down
19 changes: 16 additions & 3 deletions core/src/main/java/bisq/core/filter/FilterManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.ProofOfWorkService;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.ProofOfWork;

import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
Expand Down Expand Up @@ -493,13 +494,23 @@ public boolean isProofOfWorkValid(Offer offer) {
}
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<Integer> getEnabledPowVersions() {
Filter filter = getFilter();
return filter != null && !filter.getEnabledPowVersions().isEmpty() ? filter.getEnabledPowVersions() : List.of(0);
}


///////////////////////////////////////////////////////////////////////////////////////////
// Private
Expand Down Expand Up @@ -540,6 +551,8 @@ private void onFilterAddedFromNetwork(Filter newFilter) {
}
}

log.info("GOT NEW FILTER: {}", newFilter);

// Our new filter is newer so we apply it.
// We do not require strict guarantees here (e.g. clocks not synced) as only trusted developers have the key
// for deploying filters and this is only in place to avoid unintended situations of multiple filters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
int log2Difficulty = getPowDifficulty();
getPowService().mint(offerId, makerAddress.getFullAddress(), log2Difficulty)
.whenComplete((proofOfWork, throwable) -> {
// We got called from a non user thread...
UserThread.execute(() -> {
Expand Down Expand Up @@ -347,11 +346,9 @@ private void redoProofOfWorkAndRepublish(OpenOffer openOffer) {
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(() -> {
Expand Down Expand Up @@ -393,4 +390,16 @@ private boolean isProofOfWorkInvalid(Offer offer) {
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();
}
}

0 comments on commit 3a25b7c

Please sign in to comment.