Store difficulty as floating point in Filter & PoW

Change the type of the 'difficulty' field in the Filter & ProofOfWork
proto objects from int32/bytes to double and make it use a linear scale,
in place of the original logarithmic scale which counts the (effective)
number of required zeros.

This allows fine-grained difficulty control for Equihash, though for
Hashcash it simply rounds up to the nearest power of 2 internally.

NOTE: This is a breaking change to PoW & filter serialisation (unlike
the earlier PR commits), as the proto field version nums aren't updated.
This commit is contained in:
Steven Barclay 2021-11-26 04:06:24 +00:00
parent 0c94e232f8
commit e0595aa284
No known key found for this signature in database
GPG Key ID: 9FED6BF1176D500B
10 changed files with 77 additions and 91 deletions

View File

@ -31,7 +31,7 @@ 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. */
/** 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) {
@ -39,15 +39,15 @@ public class EquihashProofOfWorkService extends ProofOfWorkService {
}
@Override
public CompletableFuture<ProofOfWork> mint(String itemId, byte[] challenge, int log2Difficulty) {
double difficulty = adjustedDifficulty(log2Difficulty);
log.info("Got adjusted difficulty: {}", difficulty);
public CompletableFuture<ProofOfWork> mint(String itemId, byte[] challenge, double difficulty) {
double scaledDifficulty = scaledDifficulty(difficulty);
log.info("Got scaled & adjusted difficulty: {}", scaledDifficulty);
return CompletableFuture.supplyAsync(() -> {
long ts = System.currentTimeMillis();
byte[] solution = new Equihash(90, 5, difficulty).puzzle(challenge).findSolution().serialize();
byte[] solution = new Equihash(90, 5, scaledDifficulty).puzzle(challenge).findSolution().serialize();
long counter = Longs.fromByteArray(Arrays.copyOf(solution, 8));
var proofOfWork = new ProofOfWork(solution, counter, challenge, log2Difficulty,
var proofOfWork = new ProofOfWork(solution, counter, challenge, difficulty,
System.currentTimeMillis() - ts, getVersion());
log.info("Completed minting proofOfWork: {}", proofOfWork);
return proofOfWork;
@ -63,14 +63,14 @@ public class EquihashProofOfWorkService extends ProofOfWorkService {
@Override
boolean verify(ProofOfWork proofOfWork) {
double difficulty = adjustedDifficulty(proofOfWork.getNumLeadingZeros());
double scaledDifficulty = scaledDifficulty(proofOfWork.getDifficulty());
var puzzle = new Equihash(90, 5, difficulty).puzzle(proofOfWork.getChallenge());
var puzzle = new Equihash(90, 5, scaledDifficulty).puzzle(proofOfWork.getChallenge());
return puzzle.deserializeSolution(proofOfWork.getPayload()).verify();
}
private static double adjustedDifficulty(int log2Difficulty) {
return Equihash.adjustDifficulty(Math.scalb(DIFFICULTY_SCALE_FACTOR, log2Difficulty),
private static double scaledDifficulty(double difficulty) {
return Equihash.adjustDifficulty(DIFFICULTY_SCALE_FACTOR * difficulty,
Equihash.EQUIHASH_n_5_MEAN_SOLUTION_COUNT_PER_NONCE);
}
}

View File

@ -45,9 +45,9 @@ public class HashCashService extends ProofOfWorkService {
}
@Override
public CompletableFuture<ProofOfWork> mint(String itemId, byte[] challenge, int log2Difficulty) {
public CompletableFuture<ProofOfWork> mint(String itemId, byte[] challenge, double difficulty) {
byte[] payload = getBytes(itemId);
return mint(payload, challenge, log2Difficulty);
return mint(payload, challenge, difficulty);
}
@Override
@ -57,7 +57,7 @@ public class HashCashService extends ProofOfWorkService {
static CompletableFuture<ProofOfWork> mint(byte[] payload,
byte[] challenge,
int difficulty) {
double difficulty) {
return HashCashService.mint(payload,
challenge,
difficulty,
@ -68,33 +68,33 @@ public class HashCashService extends ProofOfWorkService {
boolean verify(ProofOfWork proofOfWork) {
return verify(proofOfWork,
proofOfWork.getChallenge(),
proofOfWork.getNumLeadingZeros());
toNumLeadingZeros(proofOfWork.getDifficulty()));
}
static boolean verify(ProofOfWork proofOfWork,
byte[] controlChallenge,
int controlDifficulty) {
int controlLog2Difficulty) {
return HashCashService.verify(proofOfWork,
controlChallenge,
controlDifficulty,
controlLog2Difficulty,
HashCashService::testDifficulty);
}
static boolean verify(ProofOfWork proofOfWork,
byte[] controlChallenge,
int controlDifficulty,
int controlLog2Difficulty,
BiPredicate<byte[], byte[]> challengeValidation,
BiPredicate<Integer, Integer> difficultyValidation) {
return HashCashService.verify(proofOfWork,
controlChallenge,
controlDifficulty,
controlLog2Difficulty,
challengeValidation,
difficultyValidation,
HashCashService::testDifficulty);
}
private static boolean testDifficulty(byte[] result, long difficulty) {
return HashCashService.numberOfLeadingZeros(result) > difficulty;
private static boolean testDifficulty(byte[] result, int log2Difficulty) {
return HashCashService.numberOfLeadingZeros(result) > log2Difficulty;
}
@ -104,16 +104,17 @@ public class HashCashService extends ProofOfWorkService {
static CompletableFuture<ProofOfWork> mint(byte[] payload,
byte[] challenge,
int difficulty,
double difficulty,
BiPredicate<byte[], Integer> testDifficulty) {
return CompletableFuture.supplyAsync(() -> {
long ts = System.currentTimeMillis();
int log2Difficulty = toNumLeadingZeros(difficulty);
byte[] result;
long counter = 0;
do {
result = toSha256Hash(payload, challenge, ++counter);
}
while (!testDifficulty.test(result, difficulty));
while (!testDifficulty.test(result, log2Difficulty));
ProofOfWork proofOfWork = new ProofOfWork(payload, counter, challenge, difficulty, System.currentTimeMillis() - ts, 0);
log.info("Completed minting proofOfWork: {}", proofOfWork);
return proofOfWork;
@ -122,11 +123,11 @@ public class HashCashService extends ProofOfWorkService {
static boolean verify(ProofOfWork proofOfWork,
byte[] controlChallenge,
int controlDifficulty,
int controlLog2Difficulty,
BiPredicate<byte[], Integer> testDifficulty) {
return verify(proofOfWork,
controlChallenge,
controlDifficulty,
controlLog2Difficulty,
HashCashService.isChallengeValid,
HashCashService.isDifficultyValid,
testDifficulty);
@ -134,12 +135,12 @@ public class HashCashService extends ProofOfWorkService {
static boolean verify(ProofOfWork proofOfWork,
byte[] controlChallenge,
int controlDifficulty,
int controlLog2Difficulty,
BiPredicate<byte[], byte[]> challengeValidation,
BiPredicate<Integer, Integer> difficultyValidation,
BiPredicate<byte[], Integer> testDifficulty) {
return challengeValidation.test(proofOfWork.getChallenge(), controlChallenge) &&
difficultyValidation.test(proofOfWork.getNumLeadingZeros(), controlDifficulty) &&
difficultyValidation.test(toNumLeadingZeros(proofOfWork.getDifficulty()), controlLog2Difficulty) &&
verify(proofOfWork, testDifficulty);
}
@ -147,7 +148,7 @@ public class HashCashService extends ProofOfWorkService {
byte[] hash = HashCashService.toSha256Hash(proofOfWork.getPayload(),
proofOfWork.getChallenge(),
proofOfWork.getCounter());
return testDifficulty.test(hash, proofOfWork.getNumLeadingZeros());
return testDifficulty.test(hash, toNumLeadingZeros(proofOfWork.getDifficulty()));
}
@ -194,4 +195,10 @@ public class HashCashService extends ProofOfWorkService {
}
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;
}
}

View File

@ -21,8 +21,6 @@ import bisq.common.proto.network.NetworkPayload;
import com.google.protobuf.ByteString;
import java.math.BigInteger;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@ -34,8 +32,8 @@ 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
@ -44,37 +42,9 @@ public final class ProofOfWork implements NetworkPayload {
public ProofOfWork(byte[] payload,
long counter,
byte[] challenge,
int difficulty,
double difficulty,
long duration,
int version) {
this(payload,
counter,
challenge,
BigInteger.valueOf(difficulty).toByteArray(),
duration,
version);
}
public ProofOfWork(byte[] payload,
long counter,
byte[] challenge,
BigInteger difficulty,
long duration,
int version) {
this(payload,
counter,
challenge,
difficulty.toByteArray(),
duration,
version);
}
private ProofOfWork(byte[] payload,
long counter,
byte[] challenge,
byte[] difficulty,
long duration,
int version) {
this.payload = payload;
this.counter = counter;
this.challenge = challenge;
@ -83,10 +53,6 @@ public final class ProofOfWork implements NetworkPayload {
this.version = version;
}
public int getNumLeadingZeros() {
return new BigInteger(difficulty).intValue();
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
@ -98,7 +64,7 @@ public final class ProofOfWork implements NetworkPayload {
.setPayload(ByteString.copyFrom(payload))
.setCounter(counter)
.setChallenge(ByteString.copyFrom(challenge))
.setDifficulty(ByteString.copyFrom(difficulty))
.setDifficulty(difficulty)
.setDuration(duration)
.setVersion(version)
.build();
@ -109,7 +75,7 @@ public final class ProofOfWork implements NetworkPayload {
proto.getPayload().toByteArray(),
proto.getCounter(),
proto.getChallenge().toByteArray(),
proto.getDifficulty().toByteArray(),
proto.getDifficulty(),
proto.getDuration(),
proto.getVersion()
);
@ -120,7 +86,7 @@ public final class ProofOfWork implements NetworkPayload {
public String toString() {
return "ProofOfWork{" +
",\r\n counter=" + counter +
",\r\n numLeadingZeros=" + getNumLeadingZeros() +
",\r\n difficulty=" + difficulty +
",\r\n duration=" + duration +
",\r\n version=" + version +
"\r\n}";

View File

@ -45,28 +45,28 @@ public abstract class ProofOfWorkService {
this.version = version;
}
public abstract CompletableFuture<ProofOfWork> mint(String itemId, byte[] challenge, int log2Difficulty);
public abstract CompletableFuture<ProofOfWork> mint(String itemId, byte[] challenge, double difficulty);
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 CompletableFuture<ProofOfWork> mint(String itemId, String ownerId, double difficulty) {
return mint(itemId, getChallenge(itemId, ownerId), difficulty);
}
public boolean verify(ProofOfWork proofOfWork,
String itemId,
String ownerId,
int controlLog2Difficulty,
double controlDifficulty,
BiPredicate<byte[], byte[]> challengeValidation,
BiPredicate<Integer, Integer> difficultyValidation) {
BiPredicate<Double, Double> difficultyValidation) {
Preconditions.checkArgument(proofOfWork.getVersion() == version);
byte[] controlChallenge = getChallenge(itemId, ownerId);
return challengeValidation.test(proofOfWork.getChallenge(), controlChallenge) &&
difficultyValidation.test(proofOfWork.getNumLeadingZeros(), controlLog2Difficulty) &&
difficultyValidation.test(proofOfWork.getDifficulty(), controlDifficulty) &&
verify(proofOfWork);
}
}

View File

@ -32,6 +32,17 @@ public class HashCashServiceTest {
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 {
@ -58,7 +69,8 @@ public class HashCashServiceTest {
//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();
@ -75,7 +87,7 @@ public class HashCashServiceTest {
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)

View File

@ -105,9 +105,10 @@ 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<Integer> enabledPowVersions;
@ -222,7 +223,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
boolean disableMempoolValidation,
boolean disableApi,
boolean disablePowMessage,
int powDifficulty,
double powDifficulty,
List<Integer> enabledPowVersions,
long makerFeeBtc,
long takerFeeBtc,
@ -300,7 +301,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
boolean disableMempoolValidation,
boolean disableApi,
boolean disablePowMessage,
int powDifficulty,
double powDifficulty,
List<Integer> enabledPowVersions,
long makerFeeBtc,
long takerFeeBtc,

View File

@ -90,7 +90,7 @@ public class FilterManager {
private final BiPredicate<byte[], byte[]> challengeValidation = Arrays::equals;
// We only require a new pow if difficulty has increased
private final BiPredicate<Integer, Integer> difficultyValidation =
private final BiPredicate<Double, Double> difficultyValidation =
(value, controlValue) -> value - controlValue >= 0;

View File

@ -199,8 +199,8 @@ public class OpenBsqSwapOfferService {
NodeAddress makerAddress = Objects.requireNonNull(p2PService.getAddress());
offerUtil.validateBasicOfferData(PaymentMethod.BSQ_SWAP, "BSQ");
int log2Difficulty = getPowDifficulty();
getPowService().mint(offerId, makerAddress.getFullAddress(), log2Difficulty)
double difficulty = getPowDifficulty();
getPowService().mint(offerId, makerAddress.getFullAddress(), difficulty)
.whenComplete((proofOfWork, throwable) -> {
// We got called from a non user thread...
UserThread.execute(() -> {
@ -347,8 +347,8 @@ public class OpenBsqSwapOfferService {
String newOfferId = OfferUtil.getOfferIdWithMutationCounter(openOffer.getId());
NodeAddress nodeAddress = Objects.requireNonNull(openOffer.getOffer().getMakerNodeAddress());
int log2Difficulty = getPowDifficulty();
getPowService().mint(newOfferId, nodeAddress.getFullAddress(), log2Difficulty)
double difficulty = getPowDifficulty();
getPowService().mint(newOfferId, nodeAddress.getFullAddress(), difficulty)
.whenComplete((proofOfWork, throwable) -> {
// We got called from a non user thread...
UserThread.execute(() -> {
@ -387,8 +387,8 @@ public class OpenBsqSwapOfferService {
}
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() {

View File

@ -187,7 +187,7 @@ public class FilterWindow extends Overlay<FilterWindow> {
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,
@ -270,7 +270,7 @@ public class FilterWindow extends Overlay<FilterWindow> {
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,

View File

@ -769,7 +769,7 @@ message Filter {
bool disable_api = 27;
bool disable_mempool_validation = 28;
bool disable_pow_message = 29;
int32 pow_difficulty = 30;
double pow_difficulty = 30;
repeated int32 enabled_pow_versions = 35;
int64 maker_fee_btc = 31;
int64 taker_fee_btc = 32;
@ -882,7 +882,7 @@ message ProofOfWork {
bytes payload = 1;
int64 counter = 2;
bytes challenge = 3;
bytes difficulty = 4;
double difficulty = 4;
int64 duration = 5;
int32 version = 6;
}