Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Provide ASIC resistant PoW scheme for BSQ swaps #5858

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
415 changes: 415 additions & 0 deletions common/src/main/java/bisq/common/crypto/Equihash.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<ProofOfWork> 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);
}
}
116 changes: 30 additions & 86 deletions common/src/main/java/bisq/common/crypto/HashCashService.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,124 +22,62 @@

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<byte[], byte[], Boolean> isChallengeValid = Arrays::equals;
private static final BiFunction<Integer, Integer, Boolean> isDifficultyValid = Integer::equals;

public static CompletableFuture<ProofOfWork> 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<byte[], byte[], Boolean> challengeValidation,
BiFunction<Integer, Integer, Boolean> 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<ProofOfWork> mint(byte[] payload,
@Override
public CompletableFuture<ProofOfWork> mint(byte[] payload,
byte[] challenge,
int difficulty,
BiFunction<byte[], Integer, Boolean> 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<byte[], Integer, Boolean> testDifficulty) {
return verify(proofOfWork,
controlChallenge,
controlDifficulty,
HashCashService.isChallengeValid,
HashCashService.isDifficultyValid,
testDifficulty);
}

static boolean verify(ProofOfWork proofOfWork,
byte[] controlChallenge,
int controlDifficulty,
BiFunction<byte[], byte[], Boolean> challengeValidation,
BiFunction<Integer, Integer, Boolean> difficultyValidation,
BiFunction<byte[], Integer, Boolean> testDifficulty) {
return challengeValidation.apply(proofOfWork.getChallenge(), controlChallenge) &&
difficultyValidation.apply(proofOfWork.getNumLeadingZeros(), controlDifficulty) &&
verify(proofOfWork, testDifficulty);
}

private static boolean verify(ProofOfWork proofOfWork, BiFunction<byte[], Integer, Boolean> 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);
}


///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////

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

Expand Down Expand Up @@ -178,4 +116,10 @@ static int numberOfLeadingZeros(byte i) {
}
return n - (i >>> 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;
}
}
64 changes: 21 additions & 43 deletions common/src/main/java/bisq/common/crypto/ProofOfWork.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@

import com.google.protobuf.ByteString;

import java.math.BigInteger;

import lombok.EqualsAndHashCode;
import lombok.Getter;

Expand All @@ -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;
}


Expand All @@ -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();
}

Expand All @@ -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()
);
}

Expand All @@ -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}";
}
}