diff --git a/src/main/java/jota/IotaAPIProxy.java b/src/main/java/jota/IotaAPIProxy.java index 3b4e49e..834ce13 100644 --- a/src/main/java/jota/IotaAPIProxy.java +++ b/src/main/java/jota/IotaAPIProxy.java @@ -11,6 +11,7 @@ import jota.dto.request.*; import jota.dto.response.*; +import jota.utils.IotaAPIUtils; import okhttp3.OkHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/jota/dto/response/GetNewAddressResponse.java b/src/main/java/jota/dto/response/GetNewAddressResponse.java index a9216d2..dab7d86 100644 --- a/src/main/java/jota/dto/response/GetNewAddressResponse.java +++ b/src/main/java/jota/dto/response/GetNewAddressResponse.java @@ -7,4 +7,10 @@ public class GetNewAddressResponse extends AbstractResponse { public String getAddress() { return address; } + + public static GetNewAddressResponse create(String address) { + GetNewAddressResponse res = new GetNewAddressResponse(); + res.address = address; + return res; + } } diff --git a/src/main/java/jota/utils/Converter.java b/src/main/java/jota/utils/Converter.java new file mode 100644 index 0000000..48c71e2 --- /dev/null +++ b/src/main/java/jota/utils/Converter.java @@ -0,0 +1,130 @@ +package jota.utils; + +import java.util.Arrays; + +public class Converter { + + public static final int RADIX = 3; + public static final int MAX_TRIT_VALUE = (RADIX - 1) / 2, MIN_TRIT_VALUE = -MAX_TRIT_VALUE; + + public static final int NUMBER_OF_TRITS_IN_A_BYTE = 5; + public static final int NUMBER_OF_TRITS_IN_A_TRYTE = 3; + + static final int[][] BYTE_TO_TRITS_MAPPINGS = new int[243][]; + static final int[][] TRYTE_TO_TRITS_MAPPINGS = new int[27][]; + + public static final String TRYTE_ALPHABET = "9ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + static { + + final int[] trits = new int[NUMBER_OF_TRITS_IN_A_BYTE]; + + for (int i = 0; i < 243; i++) { + BYTE_TO_TRITS_MAPPINGS[i] = Arrays.copyOf(trits, NUMBER_OF_TRITS_IN_A_BYTE); + increment(trits, NUMBER_OF_TRITS_IN_A_BYTE); + } + + for (int i = 0; i < 27; i++) { + TRYTE_TO_TRITS_MAPPINGS[i] = Arrays.copyOf(trits, NUMBER_OF_TRITS_IN_A_TRYTE); + increment(trits, NUMBER_OF_TRITS_IN_A_TRYTE); + } + } + + public static byte[] bytes(final int[] trits, final int offset, final int size) { + + final byte[] bytes = new byte[(size + NUMBER_OF_TRITS_IN_A_BYTE - 1) / NUMBER_OF_TRITS_IN_A_BYTE]; + for (int i = 0; i < bytes.length; i++) { + + int value = 0; + for (int j = (size - i * NUMBER_OF_TRITS_IN_A_BYTE) < 5 ? (size - i * NUMBER_OF_TRITS_IN_A_BYTE) : NUMBER_OF_TRITS_IN_A_BYTE; j-- > 0; ) { + value = value * RADIX + trits[offset + i * NUMBER_OF_TRITS_IN_A_BYTE + j]; + } + bytes[i] = (byte)value; + } + + return bytes; + } + + public static byte[] bytes(final int[] trits) { + return bytes(trits, 0, trits.length); + } + + public static void getTrits(final byte[] bytes, final int[] trits) { + + int offset = 0; + for (int i = 0; i < bytes.length && offset < trits.length; i++) { + System.arraycopy(BYTE_TO_TRITS_MAPPINGS[bytes[i] < 0 ? (bytes[i] + BYTE_TO_TRITS_MAPPINGS.length) : bytes[i]], 0, trits, offset, trits.length - offset < NUMBER_OF_TRITS_IN_A_BYTE ? (trits.length - offset) : NUMBER_OF_TRITS_IN_A_BYTE); + offset += NUMBER_OF_TRITS_IN_A_BYTE; + } + while (offset < trits.length) { + trits[offset++] = 0; + } + } + + public static int[] trits(final String trytes) { + + final int[] trits = new int[trytes.length() * NUMBER_OF_TRITS_IN_A_TRYTE]; + for (int i = 0; i < trytes.length(); i++) { + System.arraycopy(TRYTE_TO_TRITS_MAPPINGS[TRYTE_ALPHABET.indexOf(trytes.charAt(i))], 0, trits, i * NUMBER_OF_TRITS_IN_A_TRYTE, NUMBER_OF_TRITS_IN_A_TRYTE); + } + + return trits; + } + + public static void copyTrits(final long value, final int[] destination, final int offset, final int size) { + + long absoluteValue = value < 0 ? -value : value; + for (int i = 0; i < size; i++) { + + int remainder = (int)(absoluteValue % RADIX); + absoluteValue /= RADIX; + if (remainder > MAX_TRIT_VALUE) { + + remainder = MIN_TRIT_VALUE; + absoluteValue++; + } + destination[offset + i] = remainder; + } + + if (value < 0) { + + for (int i = 0; i < size; i++) { + destination[offset + i] = -destination[offset + i]; + } + } + } + + public static String trytes(final int[] trits, final int offset, final int size) { + + StringBuilder trytes = new StringBuilder(); + for (int i = 0; i < (size + NUMBER_OF_TRITS_IN_A_TRYTE - 1) / NUMBER_OF_TRITS_IN_A_TRYTE; i++) { + + int j = trits[offset + i * 3] + trits[offset + i * 3 + 1] * 3 + trits[offset + i * 3 + 2] * 9; + if (j < 0) { + + j += TRYTE_ALPHABET.length(); + } + trytes.append(TRYTE_ALPHABET.charAt(j)); + } + return trytes.toString(); + } + + public static String trytes(final int[] trits) { + return trytes(trits, 0, trits.length); + } + + public static int tryteValue(final int[] trits, final int offset) { + return trits[offset] + trits[offset + 1] * 3 + trits[offset + 2] * 9; + } + + public static void increment(final int[] trits, final int size) { + + for (int i = 0; i < size; i++) { + if (++trits[i] > Converter.MAX_TRIT_VALUE) { + trits[i] = Converter.MIN_TRIT_VALUE; + } else { + break; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/jota/utils/Curl.java b/src/main/java/jota/utils/Curl.java new file mode 100644 index 0000000..e8fbef8 --- /dev/null +++ b/src/main/java/jota/utils/Curl.java @@ -0,0 +1,60 @@ +package jota.utils; + +/** + * (c) 2016 Come-from-Beyond + * + * Curl belongs to the sponge function family. + * + */ +public class Curl { + + public static final int HASH_LENGTH = 243; + private static final int STATE_LENGTH = 3 * HASH_LENGTH; + + private static final int NUMBER_OF_ROUNDS = 27; + private static final int[] TRUTH_TABLE = {1, 0, -1, 1, -1, 0, -1, 1, 0}; + + private final int[] state = new int[STATE_LENGTH]; + + public void absorb(final int[] trits, int offset, int length) { + + do { + System.arraycopy(trits, offset, state, 0, length < HASH_LENGTH ? length : HASH_LENGTH); + transform(); + offset += HASH_LENGTH; + } while ((length -= HASH_LENGTH) > 0); + } + + public int [] squeeze(final int[] trits, int offset, int length) { + + do { + System.arraycopy(state, 0, trits, offset, length < HASH_LENGTH ? length : HASH_LENGTH); + transform(); + offset += HASH_LENGTH; + } while ((length -= HASH_LENGTH) > 0); + + return state; + } + + private void transform() { + + final int[] scratchpad = new int[STATE_LENGTH]; + int scratchpadIndex = 0; + for (int round = 0; round < NUMBER_OF_ROUNDS; round++) { + System.arraycopy(state, 0, scratchpad, 0, STATE_LENGTH); + for (int stateIndex = 0; stateIndex < STATE_LENGTH; stateIndex++) { + state[stateIndex] = TRUTH_TABLE[scratchpad[scratchpadIndex] + scratchpad[scratchpadIndex += (scratchpadIndex < 365 ? 364 : -365)] * 3 + 4]; + } + } + } + + public void reset() { + for (int stateIndex = 0; stateIndex < STATE_LENGTH; stateIndex++) { + state[stateIndex] = 0; + } + } + + public int[] getState() { + return state; + } +} diff --git a/src/main/java/jota/IotaAPIUtils.java b/src/main/java/jota/utils/IotaAPIUtils.java similarity index 63% rename from src/main/java/jota/IotaAPIUtils.java rename to src/main/java/jota/utils/IotaAPIUtils.java index 8de3f1b..5ec0f53 100644 --- a/src/main/java/jota/IotaAPIUtils.java +++ b/src/main/java/jota/utils/IotaAPIUtils.java @@ -1,4 +1,4 @@ -package jota; +package jota.utils; import jota.dto.response.GetBundleResponse; import jota.dto.response.GetNewAddressResponse; @@ -15,8 +15,14 @@ public class IotaAPIUtils { private static final Logger log = LoggerFactory.getLogger(IotaAPIUtils.class); - public static GetNewAddressResponse getNewAddress(final String seed, final Integer securityLevel) { - throw new NotImplementedException("Not yet implemented"); + public static GetNewAddressResponse getNewAddress(final String seed, final int index) { + + final int [] key = Signing.key(Converter.trits(seed), index, 2); + final int [] digests = Signing.digests(key); + final int [] addressTrits = Signing.address(digests); + final String address = Converter.trytes(addressTrits); + + return GetNewAddressResponse.create(address); } public static GetBundleResponse getBundle(final String transaction) { diff --git a/src/main/java/jota/utils/Signing.java b/src/main/java/jota/utils/Signing.java new file mode 100644 index 0000000..b2f53d6 --- /dev/null +++ b/src/main/java/jota/utils/Signing.java @@ -0,0 +1,93 @@ +package jota.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Signing { + + static int[] key(int [] seed, int index, int length) { + + final int[] subseed = seed; + + for (int i = 0; i < index; i++) { + for (int j = 0; j < 243; j++) { + if (++subseed[j] > 1) { + subseed[j] = -1; + } else { + break; + } + } + } + + Curl curl = new Curl(); + //curl.absorb(subseed, state); + //curl.squeeze(subseed, state); + curl.absorb(subseed, 0, subseed.length); + + List key = new ArrayList<>(); + int [] buffer = new int[subseed.length]; + int offset = 0; + + while (length-- > 0) { + + for (int i = 0; i < 27; i++) { + + curl.squeeze(buffer, 0, buffer.length); + for (int j = 0; j < 243; j++) { + key.add(buffer[j]); + } + } + } + return to(key); + } + + private static int[] to(List key) { + int a [] = new int[key.size()]; int i = 0; + for (Integer v : key) { + a[i++] = v; + } + return a; + } + + public static int [] digests(int [] key) { + final Curl curl = new Curl(); + + int[] digests = new int[key.length]; + int[] buffer = new int[key.length]; + + for (int i = 0; i < Math.floor(key.length / 6561); i++) { + int [] keyFragment = Arrays.copyOfRange(key, i * 6561, (i + 1) * 6561); + + for (int j = 0; j < 27; j++) { + + buffer = Arrays.copyOfRange(keyFragment, j * 243, (j + 1) * 243); + for (int k = 0; k < 26; k++) { + + curl.absorb(buffer, 0, buffer.length); + curl.squeeze(buffer, 0, buffer.length); + } + for (int k = 0; k < 243; k++) { + + keyFragment[j * 243 + k] = buffer[k]; + } + } + + curl.absorb(keyFragment, 0, keyFragment.length); + curl.squeeze(buffer, 0, buffer.length); + + for (int j = 0; j < 243; j++) { + digests[i * 243 + j] = buffer[j]; + } + } + return digests; + } + + public static int [] address(int [] digests) { + final Curl curl = new Curl(); + int [] address = new int[digests.length]; + curl.absorb(digests, 0, digests.length); + curl.squeeze(address, 0, address.length); + return address; + } +} diff --git a/src/test/java/jota/IotaAPIProxyTest.java b/src/test/java/jota/IotaAPIProxyTest.java index f0ffc97..c270449 100644 --- a/src/test/java/jota/IotaAPIProxyTest.java +++ b/src/test/java/jota/IotaAPIProxyTest.java @@ -5,6 +5,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import jota.dto.response.*; +import jota.utils.IotaAPIUtils; import org.hamcrest.core.IsNull; import org.junit.Before; import org.junit.Ignore; @@ -122,5 +123,11 @@ public void shouldCreateIotaApiProxyInstanceWithDefaultValues() { IotaAPIProxy proxy = new IotaAPIProxy.Builder().build(); assertThat(proxy, IsNull.notNullValue()); } - + + @Test + public void shouldCreateANewAddress() { + GetNewAddressResponse res = IotaAPIUtils.getNewAddress(TEST_SEED, 2); + System.err.println(res); + } + }