diff --git a/core/src/main/java/com/google/bitcoin/core/Base58.java b/core/src/main/java/com/google/bitcoin/core/Base58.java index f56251a0..e622b847 100644 --- a/core/src/main/java/com/google/bitcoin/core/Base58.java +++ b/core/src/main/java/com/google/bitcoin/core/Base58.java @@ -34,62 +34,132 @@ import java.util.Arrays; * */ public class Base58 { - private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; - private static final BigInteger BASE = BigInteger.valueOf(58); + private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + .toCharArray(); + private static final int BASE_58 = ALPHABET.length; + private static final int BASE_256 = 256; - public static String encode(byte[] input) { - // TODO: This could be a lot more efficient. - BigInteger bi = new BigInteger(1, input); - StringBuffer s = new StringBuffer(); - while (bi.compareTo(BASE) >= 0) { - BigInteger mod = bi.mod(BASE); - s.insert(0, ALPHABET.charAt(mod.intValue())); - bi = bi.subtract(mod).divide(BASE); - } - s.insert(0, ALPHABET.charAt(bi.intValue())); - // Convert leading zeros too. - for (byte anInput : input) { - if (anInput == 0) - s.insert(0, ALPHABET.charAt(0)); - else - break; - } - return s.toString(); - } + private static final int[] INDEXES = new int[128]; + static { + for (int i = 0; i < INDEXES.length; i++) { + INDEXES[i] = -1; + } + for (int i = 0; i < ALPHABET.length; i++) { + INDEXES[ALPHABET[i]] = i; + } + } + + public static String encode(byte[] input) { + if (input.length == 0) { + return ""; + } + input = copyOfRange(input, 0, input.length); - public static byte[] decode(String input) throws AddressFormatException { - if (input.length() == 0) { - throw new AddressFormatException("Attempt to parse an empty address."); - } - byte[] bytes = decodeToBigInteger(input).toByteArray(); - // We may have got one more byte than we wanted, if the high bit of the next-to-last byte was not zero. This - // is because BigIntegers are represented with twos-compliment notation, thus if the high bit of the last - // byte happens to be 1 another 8 zero bits will be added to ensure the number parses as positive. Detect - // that case here and chop it off. - boolean stripSignByte = bytes.length > 1 && bytes[0] == 0 && bytes[1] < 0; - // Count the leading zeros, if any. - int leadingZeros = 0; - for (int i = 0; input.charAt(i) == ALPHABET.charAt(0); i++) { - leadingZeros++; - } - // Now cut/pad correctly. Java 6 has a convenience for this, but Android can't use it. - byte[] tmp = new byte[bytes.length - (stripSignByte ? 1 : 0) + leadingZeros]; - System.arraycopy(bytes, stripSignByte ? 1 : 0, tmp, leadingZeros, tmp.length - leadingZeros); - return tmp; - } + // + // Count leading zeroes + // + int zeroCount = 0; + while (zeroCount < input.length && input[zeroCount] == 0) { + ++zeroCount; + } - public static BigInteger decodeToBigInteger(String input) throws AddressFormatException { - BigInteger bi = BigInteger.valueOf(0); - // Work backwards through the string. - for (int i = input.length() - 1; i >= 0; i--) { - int alphaIndex = ALPHABET.indexOf(input.charAt(i)); - if (alphaIndex == -1) { - throw new AddressFormatException("Illegal character " + input.charAt(i) + " at " + i); - } - bi = bi.add(BigInteger.valueOf(alphaIndex).multiply(BASE.pow(input.length() - 1 - i))); - } - return bi; - } + // + // The actual encoding + // + byte[] temp = new byte[input.length * 2]; + int j = temp.length; + + int startAt = zeroCount; + while (startAt < input.length) { + byte mod = divmod58(input, startAt); + if (input[startAt] == 0) { + ++startAt; + } + + temp[--j] = (byte) ALPHABET[mod]; + } + + // + // Strip extra '1' if there are some after decoding + // + while (j < temp.length && temp[j] == ALPHABET[0]) { + ++j; + } + + // + // Add as many leading '1' as there were leading zeros. + // + while (--zeroCount >= 0) { + temp[--j] = (byte) ALPHABET[0]; + } + + byte[] output = copyOfRange(temp, j, temp.length); + return new String(output); + } + + public static byte[] decode(String input) throws AddressFormatException { + if (input.length() == 0) { + return new byte[0]; + } + + byte[] input58 = new byte[input.length()]; + // + // Transform the String to a base58 byte sequence + // + for (int i = 0; i < input.length(); ++i) { + char c = input.charAt(i); + + int digit58 = -1; + if (c >= 0 && c < 128) { + digit58 = INDEXES[c]; + } + if (digit58 < 0) { + throw new AddressFormatException("Illegal character " + c + " at " + i); + } + + input58[i] = (byte) digit58; + } + + // + // Count leading zeroes + // + int zeroCount = 0; + while (zeroCount < input58.length && input58[zeroCount] == 0) { + ++zeroCount; + } + + // + // The encoding + // + byte[] temp = new byte[input.length()]; + int j = temp.length; + + int startAt = zeroCount; + while (startAt < input58.length) { + byte mod = divmod256(input58, startAt); + if (input58[startAt] == 0) { + ++startAt; + } + + temp[--j] = mod; + } + + // + // Do no add extra leading zeroes, move j to first non null byte. + // + while (j < temp.length && temp[j] == 0) { + ++j; + } + + return copyOfRange(temp, j - zeroCount, temp.length); + } + + public static BigInteger decodeToBigInteger(String input) throws AddressFormatException { + byte[] bytes = decode(input); + + // always return a positive BigInteger + return new BigInteger(1, bytes); + } /** * Uses the checksum in the last 4 bytes of the decoded data to verify the rest are correct. The checksum is @@ -97,19 +167,60 @@ public class Base58 { * * @throws AddressFormatException if the input is not base 58 or the checksum does not validate. */ - public static byte[] decodeChecked(String input) throws AddressFormatException { - byte[] tmp = decode(input); - if (tmp.length < 4) - throw new AddressFormatException("Input too short"); - byte[] checksum = new byte[4]; - System.arraycopy(tmp, tmp.length - 4, checksum, 0, 4); - byte[] bytes = new byte[tmp.length - 4]; - System.arraycopy(tmp, 0, bytes, 0, tmp.length - 4); - tmp = Utils.doubleDigest(bytes); - byte[] hash = new byte[4]; - System.arraycopy(tmp, 0, hash, 0, 4); - if (!Arrays.equals(hash, checksum)) - throw new AddressFormatException("Checksum does not validate"); - return bytes; - } + public static byte[] decodeChecked(String input) throws AddressFormatException { + byte tmp [] = decode(input); + if (tmp.length < 4) + throw new AddressFormatException("Input to short"); + byte[] bytes = copyOfRange(tmp, 0, tmp.length - 4); + byte[] checksum = copyOfRange(tmp, tmp.length - 4, tmp.length); + + tmp = Utils.doubleDigest(bytes); + byte[] hash = copyOfRange(tmp, 0, 4); + if (!Arrays.equals(checksum, hash)) + throw new AddressFormatException("Checksum does not validate"); + + return bytes; + } + + // + // number -> number / 58, returns number % 58 + // + private static byte divmod58(byte[] number, int startAt) { + int remainder = 0; + for (int i = startAt; i < number.length; i++) { + int digit256 = (int) number[i] & 0xFF; + int temp = remainder * BASE_256 + digit256; + + number[i] = (byte) (temp / BASE_58); + + remainder = temp % BASE_58; + } + + return (byte) remainder; + } + + // + // number -> number / 256, returns number % 256 + // + private static byte divmod256(byte[] number58, int startAt) { + int remainder = 0; + for (int i = startAt; i < number58.length; i++) { + int digit58 = (int) number58[i] & 0xFF; + int temp = remainder * BASE_58 + digit58; + + number58[i] = (byte) (temp / BASE_256); + + remainder = temp % BASE_256; + } + + return (byte) remainder; + } + + private static byte[] copyOfRange(byte[] source, int from, int to) { + byte[] range = new byte[to - from]; + System.arraycopy(source, from, range, 0, range.length); + + return range; + } + } diff --git a/core/src/test/java/com/google/bitcoin/core/Base58Test.java b/core/src/test/java/com/google/bitcoin/core/Base58Test.java index 18df1a07..481b647a 100644 --- a/core/src/test/java/com/google/bitcoin/core/Base58Test.java +++ b/core/src/test/java/com/google/bitcoin/core/Base58Test.java @@ -28,13 +28,22 @@ public class Base58Test extends TestCase { BigInteger bi = BigInteger.valueOf(3471844090L); assertEquals("16Ho7Hs", Base58.encode(bi.toByteArray())); + + byte[] zeroBytes1 = new byte[1]; + assertEquals("1", Base58.encode(zeroBytes1)); + + byte[] zeroBytes7 = new byte[7]; + assertEquals("1111111", Base58.encode(zeroBytes7)); } public void testDecode() throws Exception { byte[] testbytes = "Hello World".getBytes(); byte[] actualbytes = Base58.decode("JxF12TrwUP45BMd"); assertTrue(new String(actualbytes), Arrays.equals(testbytes, actualbytes)); - + + assertTrue("1", Arrays.equals(Base58.decode("1"), new byte[1])); + assertTrue("1111", Arrays.equals(Base58.decode("1111"), new byte[4])); + try { Base58.decode("This isn't valid base58"); fail();