/*
* This file is part of mlDHT.
*
* mlDHT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* mlDHT 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with mlDHT. If not, see <http://www.gnu.org/licenses/>.
*/
package lbms.plugins.mldht.kad;
import static the8472.utils.Arrays.compareUnsigned;
import static the8472.utils.Arrays.mismatch;
import lbms.plugins.mldht.kad.utils.ThreadLocalUtils;
import lbms.plugins.mldht.utils.Radixable;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author Damokles
*
*/
public class Key implements Radixable<Key> {
/**
* sorts the closest entries to the head, the furthest to the tail
*/
public static final class DistanceOrder implements Comparator<Key> {
final Key target;
public DistanceOrder(Key target) {
this.target = target;
}
public int compare(Key o1, Key o2) {
return target.threeWayDistance(o1, o2);
//return target.distance(o1).compareTo(target.distance(o2));
}
}
public static final Key MIN_KEY;
public static final Key MAX_KEY;
static {
MIN_KEY = new Key();
MAX_KEY = new Key();
Arrays.fill(MAX_KEY.hash, (byte)0xFF);
}
public static final int SHA1_HASH_LENGTH = 20;
public static final int KEY_BITS = SHA1_HASH_LENGTH * 8;
public static final Pattern STRING_PATTERN = Pattern.compile("[a-fA-F0-9]{40}");
final protected byte[] hash = new byte[SHA1_HASH_LENGTH];
/**
* A Key in the DHT.
*
* Key's in the distributed hash table are just SHA-1 hashes.
* Key provides all necesarry operators to be used as a value.
*/
protected Key () {
}
/**
* Clone constructor
*
* @param k Key to clone
*/
public Key (Key k) {
System.arraycopy(k.hash, 0, hash, 0, SHA1_HASH_LENGTH);
}
public Key (String hex)
{
if(hex.length() != 40)
throw new IllegalArgumentException("Hex String must have 40 bytes");
for (int i = 0; i < hex.length(); i += 2)
hash[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i+1), 16));
}
/**
* Creates a Key with this hash
*
* @param hash the SHA1 hash, has to be 20 bytes
*/
public Key (byte[] hash) {
if (hash.length != SHA1_HASH_LENGTH) {
throw new IllegalArgumentException(
"Invalid Hash must be 20bytes, was: " + hash.length);
}
System.arraycopy(hash, 0, this.hash, 0, SHA1_HASH_LENGTH);
}
public static Key setBit(int idx) {
Key k = new Key();
k.hash[idx / 8] = (byte)(0x80 >>> (idx % 8));
return k;
}
/*
* compares Keys according to their natural distance
*/
public int compareTo (Key o) {
return compareUnsigned(hash, o.hash);
}
/**
* Compares the distance of two keys relative to this one using the XOR metric
*
* @return -1 if k1 is closer to this key, 0 if k1 and k2 are equidistant, 1 if k2 is closer
*/
public int threeWayDistance(Key k1, Key k2)
{
byte[] h0 = hash;
byte[] h1 = k1.hash;
byte[] h2 = k2.hash;
int mmi = mismatch(h1, h2);
if(mmi == -1)
return 0;
int h = Byte.toUnsignedInt(h0[mmi]);
int a = Byte.toUnsignedInt(h1[mmi]);
int b = Byte.toUnsignedInt(h2[mmi]);
return Integer.compareUnsigned(a ^ h, b ^ h);
}
@Override
public boolean equals (Object o) {
if(o instanceof Key)
{
// potential alternative would be a descending comparison since prefix bytes might be shared in sorted data structures
Key otherKey = (Key) o;
return Arrays.equals(hash, otherKey.hash);
}
return false;
}
/**
* @return the hash
*/
public byte[] getHash () {
return hash.clone();
}
public void toBuffer(ByteBuffer dst) {
dst.put(hash);
}
public int getByte(int offset) {
return hash[offset];
}
public int getInt(int offset) {
byte[] hash = this.hash;
return Byte.toUnsignedInt(hash[offset]) << 24 | Byte.toUnsignedInt(hash[offset+1]) << 16 | Byte.toUnsignedInt(hash[offset+2]) << 8 | Byte.toUnsignedInt(hash[offset+3]);
}
public Key getDerivedKey(int idx) {
Key k = new Key(this);
idx = Integer.reverse(idx);
byte[] data = k.hash;
data[0] ^= (idx >>> 24) & 0xFF;
data[1] ^= (idx >>> 16) & 0xFF;
data[2] ^= (idx >>> 8) & 0xFF;
data[3] ^= idx & 0xFF;
return k;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode () {
byte[] hash = this.hash;
return (((hash[0] ^ hash[1] ^ hash[2] ^ hash[3] ^ hash[4]) & 0xff) << 24)
| (((hash[5] ^ hash[6] ^ hash[7] ^ hash[8] ^ hash[9]) & 0xff) << 16)
| (((hash[10] ^ hash[11] ^ hash[12] ^ hash[13] ^ hash[14]) & 0xff) << 8)
| ((hash[15] ^ hash[16] ^ hash[17] ^ hash[18] ^ hash[19]) & 0xff);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString () {
return toString(true);
}
public String toString(boolean nicePrint)
{
StringBuilder b = new StringBuilder(nicePrint ? 44 : 40);
for (int i = 0; i < hash.length; i++) {
if (nicePrint && i % 4 == 0 && i > 0) {
b.append(' ');
}
int nibble = (hash[i] & 0xF0) >> 4;
b.append((char)(nibble < 0x0A ? '0'+nibble : 'A'+nibble-10 ));
nibble = hash[i] & 0x0F;
b.append((char)(nibble < 0x0A ? '0'+nibble : 'A'+nibble-10 ));
}
return b.toString();
}
public String toBinString() {
StringBuilder builder = new StringBuilder(160);
for(int i=0;i<160;i++)
builder.append((hash[i/8] & (0x80 >> (i % 8))) != 0 ? '1' : '0');
return builder.toString();
}
/**
* Returns the approximate distance of this key to the other key.
*
* Distance is simplified by returning the index of the first different Bit.
*
* @param id Key to compare to.
* @return integer marking the different bits of the keys
*/
public int findApproxKeyDistance (Key id) {
// XOR our id and the sender's ID
Key d = Key.distance(id, this);
return d.leadingOneBit();
}
public int leadingOneBit() {
byte[] data_hash = hash;
for (int i = 0; i < 20; i++) {
// get the byte
int b = data_hash[i] & 0xFF;
// no bit on in this byte so continue
if (b == 0) {
continue;
}
return i * 8 + Integer.numberOfLeadingZeros(b) - 24;
}
return -1;
}
/**
* Calculates the distance between two Keys.
*
* The distance is basically a XOR of both key hashes.
*
* @param x
* @return new Key (this.hash ^ x.hash);
*/
public Key distance (Key x) {
return distance(this, x);
}
public Key add(Key x) {
int carry = 0;
Key out = new Key(this);
for(int i=19;i>=0;i--) {
carry = Byte.toUnsignedInt(out.hash[i]) + Byte.toUnsignedInt(x.hash[i]) + carry;
out.hash[i] = (byte)(carry & 0xff);
carry >>>= 8;
}
return out;
}
/**
* calculates log2(this - otherKey % 2^161).<br />
* To get the natural distance for ascending key order this should be the successive element of otherKey
*/
public double naturalDistance(Key otherKey) {
return Math.log(new BigInteger(1,hash).subtract(new BigInteger(1, otherKey.hash)).mod(new BigInteger(1,MAX_KEY.hash).add(new BigInteger("1"))).doubleValue())/Math.log(2);
}
/**
* Calculates the distance between two Keys.
*
* The distance is basically a XOR of both key hashes.
*
* @param a
* @param b
* @return new Key (a.hash ^ b.hash);
*/
public static Key distance (Key a, Key b) {
Key x = new Key();
for (int i = 0; i < a.hash.length; i++) {
x.hash[i] = (byte) (a.hash[i] ^ b.hash[i]);
}
return x;
}
/**
* Creates a random Key
*
* @return newly generated random Key
*/
public static Key createRandomKey () {
Key x = new Key();
ThreadLocalUtils.getThreadLocalRandom().nextBytes(x.hash);
return x;
}
public int getRadix(int byteIndex) {
return hash[byteIndex] & 0xFF;
}
public static void main(String[] args) {
/*
Key target = new Key();
target.hash[0] = (byte) 0xF0;
Key test1 = new Key();
test1.hash[0] = (byte) 0x80;
Key test2 = new Key();
test2.hash[0] = 0x03;
System.out.println(test1.compareTo(test2));
System.out.println(target.distance(test1).compareTo(target.distance(test2)));
System.out.println(target.threeWayDistance(test1, test2));
*/
/*
// simulation to check that natural order != xor order
Random rand = new Random();
for(int i=0;i<10000;i++)
{
ArrayList<Key> keys = new ArrayList<Key>();
for(int j=0;j<100000;j++)
keys.add(Key.createRandomKey());
Collections.sort(keys);
for(int j=1;j<keys.size();j++)
if(keys.get(j-1).equals(keys.get(j)))
{
j--;
keys.remove(j);
}
Key[] keysArray = keys.toArray(new Key[keys.size()]);
for(int j=0;j<1000;j++)
{
Key target = Key.createRandomKey();
int closestSetSize = rand.nextInt(12);
TreeSet<Key> referenceClosestSet = new TreeSet<Key>(new DistanceOrder(target));
referenceClosestSet.addAll(keys);
List<Key> closestSet1 = new ArrayList<Key>();
for(Key closest : referenceClosestSet)
{
if(closestSet1.size() == closestSetSize)
break;
closestSet1.add(closest);
}
List<Key> closestSet2 = new ArrayList<Key>();
int startIdx = Arrays.binarySearch(keysArray, target);
if(startIdx < 0)
startIdx = startIdx*-1 - 1;
closestSet2.add(keysArray[startIdx]);
for(int k = 1;closestSet2.size() < closestSetSize;k++)
{
if(startIdx-k >= 0)
closestSet2.add(keysArray[startIdx-k]);
if(startIdx+k < keysArray.length)
closestSet2.add(keysArray[startIdx+k]);
}
Collections.sort(closestSet2,new DistanceOrder(target));
if(closestSet2.size() > closestSetSize)
closestSet2.subList(closestSetSize, closestSet2.size()).clear();
for(int k=0;k<closestSet1.size();k++)
System.out.print(Arrays.binarySearch(keysArray, closestSet1.get(k))+" ");
System.out.println();
for(int k=0;k<closestSet2.size();k++)
System.out.print(Arrays.binarySearch(keysArray, closestSet2.get(k))+" ");
System.out.println("\n");
if(!closestSet1.equals(closestSet2))
System.out.println("damn");
}
}*/
/*
// simulation to determine the error introduced by natural order iteration vs. xor order
try
{
Random rand = new Random();
TreeMap<Double, Integer> binningNat = new TreeMap<Double, Integer>();
TreeMap<Double, Integer> binningXor = new TreeMap<Double, Integer>();
ArrayList<Key> keyspace = new ArrayList<Key>(5000000);
for (int i = 0; i < 5000000; i++)
keyspace.add(Key.createRandomKey());
Collections.sort(keyspace);
for (int i = 1; i < keyspace.size(); i++)
{
Key prev = keyspace.get(i - 1);
Key curr = keyspace.get(i);
Key xorDist = prev.distance(curr);
double l2xor = 160 - Math.log(new BigInteger(1, xorDist.hash).doubleValue()) / Math.log(2);
double l2nat = 160 - Math.log(new BigInteger(1,curr.hash).subtract(new BigInteger(1, prev.hash)).doubleValue())/Math.log(2);
double roundedX = ((int)(l2xor*10))/10.0;
double roundedN = ((int)(l2nat*10))/10.0;
if(binningNat.containsKey(roundedN))
binningNat.put(roundedN, binningNat.get(roundedN)+1);
else
binningNat.put(roundedN, 1);
if(binningXor.containsKey(roundedX))
binningXor.put(roundedX, binningXor.get(roundedX)+1);
else
binningXor.put(roundedX, 1);
}
System.out.println("natural");
for(Map.Entry<Double, Integer> e : binningNat.entrySet()) {
System.out.println(e.getKey()+"\t"+e.getValue());
}
System.out.println("xor");
for(Map.Entry<Double, Integer> e : binningXor.entrySet()) {
System.out.println(e.getKey()+"\t"+e.getValue());
} } catch (Exception e)
{
e.printStackTrace();
}
*/
/* // checking some binary arithmetic
byte b1 = (byte) 0x7F;
byte b2 = (byte) 0x80;
int res = (b2 & 0xFF) - (b1 & 0xFF);
System.out.println(res);
if(res > 0)
System.out.println("b2 > b1");
if(res < 0)
System.out.println("b2 < b1");
*/
// checking some bigint arithmetic
/*
Key k1 = new Key(MIN_KEY);
k1.hash[19] = (byte) 0x80;
Key k2 = new Key(MIN_KEY);
System.out.println(Math.log(k1.naturalDistance(k2))/Math.log(2));
*/
//System.out.println(Key.MIN_KEY.getDerivedKey(0xfffffff0));
Prefix p = Prefix.WHOLE_KEYSPACE;
for(int i=0;i<64;i++)
p = p.splitPrefixBranch(false);
for(int i=0;i<10;i++)
{
Set<Key> s = new HashSet<>();
for(int j=0;j<100000;j++)
s.add(p.createRandomKeyFromPrefix());
long nanoTime = System.nanoTime();
for(int j=0;j<100000;j++)
s.contains(p.createRandomKeyFromPrefix());
System.out.println((System.nanoTime()-nanoTime)/(1000*1000));
}
}
}