diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketConfig.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketConfig.java new file mode 100644 index 0000000000..a63f008649 --- /dev/null +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketConfig.java @@ -0,0 +1,75 @@ +/* + * 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.desktop.components.cathash; + +import bisq.common.util.MathUtils; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BucketConfig { + static final String DIGIT = "#"; + + private static final int BG0_COUNT = 16; + private static final int BG1_COUNT = 16; + private static final int EARS0_COUNT = 16; + private static final int EARS1_COUNT = 3; + private static final int FACE0_COUNT = 16; + private static final int FACE1_COUNT = 9; + private static final int EYES0_COUNT = 16; + private static final int NOSE0_COUNT = 6; + private static final int WHISKERS0_COUNT = 7; + + private static final int[] BUCKET_SIZES = new int[]{BG0_COUNT, BG1_COUNT, EARS0_COUNT, EARS1_COUNT, FACE0_COUNT, + FACE1_COUNT, EYES0_COUNT, NOSE0_COUNT, WHISKERS0_COUNT}; + + private static final String[] PATH_TEMPLATES; + + static { + String postFix = ".png"; + PATH_TEMPLATES = new String[]{ + "bg0/" + DIGIT + postFix, + "bg1/" + DIGIT + postFix, + "ears0/" + DIGIT + postFix, + "ears1/" + DIGIT + postFix, + "face0/" + DIGIT + postFix, + "face1/" + DIGIT + postFix, + "eyes0/" + DIGIT + postFix, + "nose0/" + DIGIT + postFix, + "whiskers0/" + DIGIT + postFix + }; + + long numCombinations = getNumCombinations(); + log.info("Number of combinations: 2^{} = {}", MathUtils.getLog2(numCombinations), numCombinations); + } + + static int[] getBucketSizes() { + return BUCKET_SIZES; + } + + static String[] getPathTemplates() { + return PATH_TEMPLATES; + } + + static long getNumCombinations() { + long result = 1; + for (int bucketSize : BUCKET_SIZES) { + result *= bucketSize; + } + return result; + } +} diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketEncoder.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketEncoder.java new file mode 100644 index 0000000000..d6288e546e --- /dev/null +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/BucketEncoder.java @@ -0,0 +1,53 @@ +/* + * 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.desktop.components.cathash; + +import java.math.BigInteger; + +public class BucketEncoder { + /** + * @param input A BigInteger input that is to be split up deterministically in buckets according to the bucketSizes array. + * @return buckets + */ + static int[] encode(BigInteger input, int[] bucketSizes) { + int currentBucket = 0; + int[] result = new int[bucketSizes.length]; + while (currentBucket < bucketSizes.length) { + int bucketSize = bucketSizes[currentBucket]; + BigInteger[] divisorReminder = input.divideAndRemainder(BigInteger.valueOf(bucketSize)); + input = divisorReminder[0]; + long reminder = divisorReminder[1].longValue(); + result[currentBucket] = (int) Math.abs(reminder % bucketSize); + currentBucket++; + } + return result; + } + + static String[] toPaths(int[] buckets, String[] pathTemplates) { + String[] paths = new String[buckets.length]; + for (int facet = 0; facet < buckets.length; facet++) { + int bucketValue = buckets[facet]; + paths[facet] = generatePath(pathTemplates[facet], bucketValue); + } + return paths; + } + + private static String generatePath(String pathTemplate, int index) { + return pathTemplate.replaceAll(BucketConfig.DIGIT, String.format("%02d", index)); + } +} \ No newline at end of file diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/CatHash.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/CatHash.java index 41513e253e..ceb0849b1a 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/CatHash.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/CatHash.java @@ -26,11 +26,10 @@ import java.util.concurrent.ConcurrentHashMap; // Derived from https://github.com/neuhalje/android-robohash -// Number of combinations: 3 * 15 * 15 * 15 * 15 * 15 * 15 = 34171875 (2 ^ 25) @Slf4j public class CatHash { + private static final int SIZE = 300; private static final int MAX_CACHE_SIZE = 10000; - private static final HandleFactory HANDLE_FACTORY = new HandleFactory(); private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); public static Image getImage(byte[] pubKeyHash) { @@ -45,23 +44,13 @@ private static Image getImage(ByteArray pubKeyHash, boolean useCache) { if (useCache && CACHE.containsKey(pubKeyHash)) { return CACHE.get(pubKeyHash); } - BigInteger bigInteger = new BigInteger(pubKeyHash.getBytes()); - Configuration configuration = new Configuration(); - VariableSizeHashing hashing = new VariableSizeHashing(configuration.getBucketSizes()); - byte[] data = hashing.createBuckets(bigInteger); - Handle handle = HANDLE_FACTORY.calculateHandle(data); - Image image = imageForHandle(handle, configuration); + BigInteger input = new BigInteger(pubKeyHash.getBytes()); + int[] buckets = BucketEncoder.encode(input, BucketConfig.getBucketSizes()); + String[] paths = BucketEncoder.toPaths(buckets, BucketConfig.getPathTemplates()); + Image image = ImageUtil.composeImage(paths, SIZE, SIZE); if (useCache && CACHE.size() < MAX_CACHE_SIZE) { CACHE.put(pubKeyHash, image); } return image; } - - private static Image imageForHandle(Handle handle, Configuration configuration) { - long ts = System.currentTimeMillis(); - byte[] bucketValues = handle.bucketValues(); - String[] paths = configuration.convertToFacetParts(bucketValues); - log.debug("Generated paths for CatHash image in {} ms", System.currentTimeMillis() - ts); // typically <1ms - return ImageUtil.composeImage(paths, configuration.width(), configuration.height()); - } } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/Configuration.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/Configuration.java deleted file mode 100644 index 44b37be2de..0000000000 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/Configuration.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.desktop.components.cathash; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class Configuration { - private final static String ROOT = ""; - - private static final int BG0_COUNT = 15; - private static final int BG1_COUNT = 15; - private static final int EARS0_COUNT = 15; - private static final int EARS1_COUNT = 2; - private static final int FACE0_COUNT = 15; - private static final int FACE1_COUNT = 8; - private static final int EYES0_COUNT = 15; - private static final int NOSE0_COUNT = 5; - private static final int WHISKERS0_COUNT = 6; - - private final static int BUCKET_COUNT = 9; - private final static int FACET_COUNT = 9; - - private final static byte[] BUCKET_SIZES = new byte[]{BG0_COUNT, BG1_COUNT, EARS0_COUNT, EARS1_COUNT, FACE0_COUNT, - FACE1_COUNT, EYES0_COUNT, NOSE0_COUNT, WHISKERS0_COUNT}; - - private final static String[] FACET_PATH_TEMPLATES; - - static { - String postFix = ".png"; - FACET_PATH_TEMPLATES = new String[]{ - ROOT + "bg0/#ITEM#" + postFix, - ROOT + "bg1/#ITEM#" + postFix, - ROOT + "ears0/#ITEM#" + postFix, - ROOT + "ears1/#ITEM#" + postFix, - ROOT + "face0/#ITEM#" + postFix, - ROOT + "face1/#ITEM#" + postFix, - ROOT + "eyes0/#ITEM#" + postFix, - ROOT + "nose0/#ITEM#" + postFix, - ROOT + "whiskers0/#ITEM#" + postFix, - }; - } - - public String[] convertToFacetParts(byte[] bucketValues) { - if (bucketValues.length != BUCKET_COUNT) { - throw new IllegalArgumentException(); - } - - String[] paths = new String[FACET_COUNT]; - for (int facet = 0; facet < FACET_COUNT; facet++) { - int bucketValue = bucketValues[facet]; - paths[facet] = generatePath(FACET_PATH_TEMPLATES[facet], bucketValue); - } - return paths; - } - - private String generatePath(String facetPathTemplate, int bucketValue) { - return facetPathTemplate.replaceAll("#ITEM#", String.format("%02d", bucketValue)); - } - - public byte[] getBucketSizes() { - return BUCKET_SIZES; - } - - public int width() { - return 300; - } - - public int height() { - return 300; - } -} diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/Handle.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/Handle.java deleted file mode 100644 index 7c057457b4..0000000000 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/Handle.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.desktop.components.cathash; - -public final class Handle { - private final long value; - - Handle(long v) { - this.value = v; - } - - @Override - protected Object clone() throws CloneNotSupportedException { - return super.clone(); - } - - @Override - public boolean equals(Object o) { - return super.equals(o); - } - - @Override - public String toString() { - return String.format("%016X", value); - } - - @Override - public int hashCode() { - return (int) value; - } - - public byte[] bucketValues() { - return HandleFactory.bucketValues(this.value); - } -} diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/HandleFactory.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/HandleFactory.java deleted file mode 100644 index fcb19fa693..0000000000 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/HandleFactory.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.desktop.components.cathash; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class HandleFactory { - - public Handle calculateHandle(byte[] data) { - return new Handle(calculateHandleValue(data)); - } - - /** - * Encodes an array of bytes (data) into a single long value, which serves as a compact, unique identifier - * (or "handle") for a set of values. Each byte in the array represents a "nibble" (a 4-bit value), and the function - * ensures that these values, along with the length of the data, are packed into the returned long. - * - * @param data The distributed hash over the buckets - * @return val The composite handle value, which encodes both the sequence of nibbles and the length of the data array - */ - static long calculateHandleValue(byte[] data) { - // Check if the input array exceeds the maximum length of 14 bytes. - // This limit ensures that the data can be encoded into a 64-bit long value without overflow. - // Since 8 bits are reserved for length encoding only 56 bits can be used (14 * 4) - // This means that the maximum number of buckets that we can have is 14 - if (data.length > 14) { - throw new IllegalArgumentException(); - } - - long val = 0; - for (int i = 0; i < data.length; i++) { - int nibble = data[i]; - - // Validate that the current nibble does not exceed the maximum value of 15 (0xF), ensuring it's a - // valid 4-bit value. - // Each nibble uses 4 bits, therefore we can only encode 2^4 (0..15) possibilities - // (i.e. max size per bucket is 15, which represent 16 images) - if (nibble > 15) { // 0xf - throw new IllegalArgumentException(String.format("nibble to large @%d: %02X", i, nibble)); - } - - // Shift the current handle value 4 bits to the left to make room for the new nibble. This operation - // progressively builds up the handle value from its constituent nibbles. - val <<= 4; - - // Incorporate the current nibble into the lowest 4 bits of the handle value. - val |= nibble; - } - - // After processing all nibbles, encode the length of the data array into the handle value. - // This is achieved by shifting the length leftward by (14 * 4) bits (56 bits), which positions the length - // information in the upper 8 bits of the 64-bit long value. This ensures the length can be retrieved from the - // handle and also contributes to the uniqueness of the handle. - val |= ((long) data.length) << (14 * 4); - return val; - } - - static byte getNibbleAt(long value, int index) { - if (index < 0 || index > 15) { - throw new IllegalArgumentException(String.format("index @%d", index)); - } - - long mask = (long) 0xf << (index * 4); - long maskedValue = (value & mask); - - return (byte) (maskedValue >> index * 4); - } - - static int getSize(long value) { - return getNibbleAt(value, 14); - } - - public static byte[] bucketValues(long handle) { - int buckets = getSize(handle); - byte[] values = new byte[buckets]; - for (int i = 0; i < buckets; i++) { - values[buckets - i - 1] = getNibbleAt(handle, i); - } - return values; - } -} diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/VariableSizeHashing.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/VariableSizeHashing.java deleted file mode 100644 index e117b036b9..0000000000 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/components/cathash/VariableSizeHashing.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.desktop.components.cathash; - -import java.math.BigInteger; -import java.util.UUID; - -/** - * "Hash" a big integer (expected: hash or uuid) into buckets. The goal is to deterministically - * "repack" the randomness of the hash into the bucket. - *

- * Each bucket is defined by a maximum value. The implementation guarantees that the values in bucket n is in the - * range 0..(bucketSize[n]-1). - */ -public class VariableSizeHashing { - private final byte[] bucketSizes; - - public VariableSizeHashing(byte[] bucketSizes) { - this.bucketSizes = bucketSizes; - } - - static BigInteger uuidToBigInteger(UUID uuid) { - return BigInteger.valueOf(uuid.getMostSignificantBits()).shiftLeft(64).add(BigInteger.valueOf(uuid.getLeastSignificantBits())); - } - - public byte[] createBuckets(UUID uuid) { - return createBuckets(uuidToBigInteger(uuid)); - } - - /** - * Takes the hash value and distributes it over the buckets. - *

- * Assumption: the value of hash is (much) larger than `16^bucketSizes.length` and uniformly distributed (random) - * - * @param hash Any BigInteger that is to be split up in buckets according to the bucket configuration #bucketSizes. - * @return buckets The distributed hash - */ - public byte[] createBuckets(BigInteger hash) { - int currentBucket = 0; - byte[] ret = new byte[this.bucketSizes.length]; - - while (currentBucket < this.bucketSizes.length) { - BigInteger[] divisorReminder = hash.divideAndRemainder(BigInteger.valueOf(bucketSizes[currentBucket])); - - hash = divisorReminder[0]; - long reminder = divisorReminder[1].longValue(); - - ret[currentBucket] = (byte) Math.abs(reminder % bucketSizes[currentBucket]); - - currentBucket += 1; - } - - return ret; - } -} diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/overlay/onboarding/create_profile/CreateProfileController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/overlay/onboarding/create_profile/CreateProfileController.java index 29b91ccd3e..27b3ab1ac1 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/overlay/onboarding/create_profile/CreateProfileController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/overlay/onboarding/create_profile/CreateProfileController.java @@ -22,8 +22,8 @@ import bisq.desktop.common.threading.UIThread; import bisq.desktop.common.view.Controller; import bisq.desktop.common.view.Navigation; -import bisq.desktop.components.overlay.Popup; import bisq.desktop.components.cathash.CatHash; +import bisq.desktop.components.overlay.Popup; import bisq.desktop.overlay.OverlayController; import bisq.i18n.Res; import bisq.identity.IdentityService; diff --git a/common/src/main/java/bisq/common/util/MathUtils.java b/common/src/main/java/bisq/common/util/MathUtils.java index da046d4177..16428cf4ec 100644 --- a/common/src/main/java/bisq/common/util/MathUtils.java +++ b/common/src/main/java/bisq/common/util/MathUtils.java @@ -108,4 +108,8 @@ public static long bounded(long lowerBound, long upperBound, long value) { "lowerBound must not be larger than upperBound"); return Math.min(Math.max(value, lowerBound), upperBound); } + + public static double getLog2(long value) { + return Math.log(value) / Math.log(2); + } }