Support for discovering the network parameters from an address. Different exception type for wrong network parameters so chain-crossing can be handled differently.

This commit is contained in:
Mike Hearn
2012-04-02 12:23:29 +02:00
parent 11117dacbe
commit 9075561993
8 changed files with 227 additions and 80 deletions

View File

@@ -17,7 +17,7 @@
package com.google.bitcoin.core;
/**
* A BitCoin address is fundamentally derived from an elliptic curve public key and a set of network parameters.
* A Bitcoin address is derived from an elliptic curve public key and a set of network parameters.
* It has several possible representations:<p>
*
* <ol>
@@ -26,6 +26,9 @@ package com.google.bitcoin.core;
* <li>A base58 encoded "human form" that includes a version and check code, to guard against typos.
* </ol><p>
*
* The most common written form is the latter, and there may be several different types of address with the meaning
* determined by the version code.<p>
*
* One may question whether the base58 form is really an improvement over the hash160 form, given
* they are both very unfriendly for typists. More useful representations might include qrcodes
* and identicons.<p>
@@ -47,17 +50,78 @@ public class Address extends VersionedChecksummedBytes {
/**
* Construct an address from parameters and the standard "human readable" form. Example:<p>
*
* <pre>new Address(NetworkParameters.prodNet(), "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL");</pre>
* <pre>new Address(NetworkParameters.prodNet(), "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL");</pre><p>
*
* @param params The expected NetworkParameters or null if you don't want validation.
* @param address The textual form of the address, such as "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL"
* @throws AddressFormatException if the given address doesn't parse or the checksum is invalid
* @throws WrongNetworkException if the given address is valid but for a different chain (eg testnet vs prodnet)
*/
public Address(NetworkParameters params, String address) throws AddressFormatException {
public Address(NetworkParameters params, String address) throws AddressFormatException, WrongNetworkException {
super(address);
if (version != params.addressHeader)
throw new AddressFormatException("Mismatched version number, trying to cross networks? " + version +
" vs " + params.addressHeader);
if (params != null) {
boolean found = false;
for (int v : params.acceptableAddressCodes) {
if (version == v) {
found = true;
break;
}
}
if (!found) {
throw new WrongNetworkException(version, params.acceptableAddressCodes);
}
}
}
/** The (big endian) 20 byte hash that is the core of a BitCoin address. */
public byte[] getHash160() {
return bytes;
}
/**
* Examines the version byte of the address and attempts to find a matching NetworkParameters. If you aren't sure
* which network the address is intended for (eg, it was provided by a user), you can use this to decide if it is
* compatible with the current wallet. You should be able to handle a null response from this method. Note that the
* parameters returned is not necessarily the same as the one the Address was created with.
*
* @return a NetworkParameters representing the network the address is intended for, or null if unknown.
*/
public NetworkParameters getParameters() {
// TODO: There should be a more generic way to get all supported networks.
NetworkParameters[] networks =
new NetworkParameters[] { NetworkParameters.testNet(), NetworkParameters.prodNet() };
for (NetworkParameters params : networks) {
if (params.acceptableAddressCodes == null) {
// Old Java-serialized wallet. This code can eventually be deleted.
if (params.getId().equals(NetworkParameters.ID_PRODNET))
params = NetworkParameters.prodNet();
else if (params.getId().equals(NetworkParameters.ID_TESTNET))
params = NetworkParameters.testNet();
}
for (int code : params.acceptableAddressCodes) {
if (code == version) {
return params;
}
}
}
return null;
}
/**
* Given an address, examines the version byte and attempts to find a matching NetworkParameters. If you aren't sure
* which network the address is intended for (eg, it was provided by a user), you can use this to decide if it is
* compatible with the current wallet. You should be able to handle a null response from this method.
*
* @param address
* @return a NetworkParameters representing the network the address is intended for, or null if unknown.
*/
public static NetworkParameters getParametersFromAddress(String address) throws AddressFormatException {
try {
return new Address(null, address).getParameters();
} catch (WrongNetworkException e) {
// Cannot happen.
throw new RuntimeException(e);
}
}
}

View File

@@ -58,6 +58,9 @@ public class Base58 {
}
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

View File

@@ -73,7 +73,11 @@ public class NetworkParameters implements Serializable {
public int port;
/** The header bytes that identify the start of a packet on this network. */
public long packetMagic;
/** First byte of a base58 encoded address. See {@link Address}*/
/**
* First byte of a base58 encoded address. See {@link Address}. This is the same as acceptableAddressCodes[0] and
* is the one used for "normal" addresses. Other types of address may be encountered with version codes found in
* the acceptableAddressCodes array.
*/
public int addressHeader;
/** First byte of a base58 encoded dumped private key. See {@link DumpedPrivateKey}. */
public int dumpedPrivateKeyHeader;
@@ -96,7 +100,14 @@ public class NetworkParameters implements Serializable {
* by looking at the port number.
*/
private String id;
/**
* The version codes that prefix addresses which are acceptable on this network. Although Satoshi intended these to
* be used for "versioning", in fact they are today used to discriminate what kind of data is contained in the
* address and to prevent accidentally sending coins across chains which would destroy them.
*/
public int[] acceptableAddressCodes;
private static Block createGenesis(NetworkParameters n) {
Block genesisBlock = new Block(n);
Transaction t = new Transaction(n);
@@ -132,6 +143,7 @@ public class NetworkParameters implements Serializable {
n.packetMagic = 0xfabfb5daL;
n.port = 18333;
n.addressHeader = 111;
n.acceptableAddressCodes = new int[] { 111 };
n.dumpedPrivateKeyHeader = 239;
n.interval = INTERVAL;
n.targetTimespan = TARGET_TIMESPAN;
@@ -160,6 +172,7 @@ public class NetworkParameters implements Serializable {
n.port = 8333;
n.packetMagic = 0xf9beb4d9L;
n.addressHeader = 0;
n.acceptableAddressCodes = new int[] { 0 };
n.dumpedPrivateKeyHeader = 128;
n.interval = INTERVAL;
n.targetTimespan = TARGET_TIMESPAN;

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.core;
import java.util.Arrays;
/**
* This exception is thrown by the Address class when you try and decode an address with a version code that isn't
* used by that network. You shouldn't allow the user to proceed in this case as they are trying to send money across
* different chains, an operation that is guaranteed to destroy the money.
*/
public class WrongNetworkException extends AddressFormatException {
/** The version code that was provided in the address. */
public int verCode;
/** The list of acceptable versions that were expected given the addresses network parameters. */
public int[] acceptableVersions;
public WrongNetworkException(int verCode, int[] acceptableVersions) {
super("Version code of address did not match acceptable versions for network: " + verCode + " not in " +
Arrays.toString(acceptableVersions));
this.verCode = verCode;
this.acceptableVersions = acceptableVersions;
}
}

View File

@@ -34,27 +34,26 @@ import java.util.LinkedHashMap;
import java.util.Map;
/**
* <p>
* Provides a standard implementation of a Bitcoin URI with support for the
* following:
* </p>
* <p>Provides a standard implementation of a Bitcoin URI with support for the
* following:</p>
*
* <ul>
* <li>URLEncoded URIs (as passed in by IE on the command line)</li>
* <li>BIP21 names (including the "req-" prefix handling requirements)</li>
* </ul>
*
* <h2>Accepted formats</h2>
* <p>
* The following input forms are accepted
* </p>
*
* <p>The following input forms are accepted:</p>
*
* <ul>
* <li>{@code bitcoin:<address>}</li>
* <li>{@code bitcoin:<address>?<name1>=<value1>&<name2>=<value2>} with multiple
* additional name/value pairs</li>
* </ul>
* <p>
* The name/value pairs are processed as follows:
* </p>
* <ul>
*
* <p>The name/value pairs are processed as follows.</p>
* <ol>
* <li>URL encoding is stripped and treated as UTF-8</li>
* <li>names prefixed with {@code req-} are treated as required and if unknown
* or conflicting cause a parse exception</li>
@@ -62,10 +61,9 @@ import java.util.Map;
* by parameter name</li>
* <li>Known names not prefixed with {@code req-} are processed unless they are
* malformed</li>
* </ul>
* <p>
* The following names are known and have the following formats
* </p>
* </ol>
*
* <p>The following names are known and have the following formats</p>
* <ul>
* <li>{@code amount} decimal value to 8 dp (e.g. 0.12345678) <b>Note that the
* exponent notation is not supported any more</b></li>
@@ -85,10 +83,10 @@ public class BitcoinURI {
private static final Logger log = LoggerFactory.getLogger(BitcoinURI.class);
// Not worth turning into an enum
private static final String FIELD_MESSAGE = "message";
private static final String FIELD_LABEL = "label";
private static final String FIELD_AMOUNT = "amount";
private static final String FIELD_ADDRESS = "address";
public static final String FIELD_MESSAGE = "message";
public static final String FIELD_LABEL = "label";
public static final String FIELD_AMOUNT = "amount";
public static final String FIELD_ADDRESS = "address";
public static final String BITCOIN_SCHEME = "bitcoin";
private static final String ENCODED_SPACE_CHARACTER = "%20";
@@ -102,7 +100,7 @@ public class BitcoinURI {
private final Map<String, Object> parameterMap = new LinkedHashMap<String, Object>();
/**
* @param networkParameters
* @param params
* The BitCoinJ network parameters that determine which network
* the URI is from
* @param input
@@ -111,16 +109,16 @@ public class BitcoinURI {
* @throws BitcoinURIParseException
* If the input fails Bitcoin URI syntax and semantic checks
*/
public BitcoinURI(NetworkParameters networkParameters, String input) {
public BitcoinURI(NetworkParameters params, String input) {
// Basic validation
if (networkParameters == null) {
if (params == null) {
throw new IllegalArgumentException("NetworkParameters cannot be null");
}
if (input == null) {
throw new IllegalArgumentException("Input cannot be null");
}
log.debug("Attempting to parse '{}' for {}", input, networkParameters.port == 8333 ? "prodNet" : "testNet");
log.debug("Attempting to parse '{}' for {}", input, params.getId());
// URI validation
if (!input.startsWith(BITCOIN_SCHEME)) {
@@ -137,16 +135,17 @@ public class BitcoinURI {
// URI is formed as bitcoin:<address>?<query parameters>
// Remove the bitcoin scheme
// Remove the bitcoin scheme.
// (Note: getSchemeSpecificPart() is not used as it unescapes the label and parse then fails.
// For instance with : bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=0.06&label=Tom%20%26%20Jerry
// the & (%26) in Tom and Jerry gets interpreted as a separator and the label then gets parsed as 'Tom ' instead of 'Tom & Jerry')
// the & (%26) in Tom and Jerry gets interpreted as a separator and the label then gets parsed
// as 'Tom ' instead of 'Tom & Jerry')
String schemeSpecificPart = "";
if (uri.toString().startsWith(BITCOIN_SCHEME + COLON_SEPARATOR)) {
schemeSpecificPart = uri.toString().substring(BITCOIN_SCHEME.length() + 1);
}
// Split off the address from the rest of the query parameters
// Split off the address from the rest of the query parameters.
String[] addressSplitTokens = schemeSpecificPart.split("\\?");
if (addressSplitTokens.length == 0 || "".equals(addressSplitTokens[0])) {
throw new BitcoinURIParseException("Missing address");
@@ -155,51 +154,51 @@ public class BitcoinURI {
String[] nameValuePairTokens;
if (addressSplitTokens.length == 1) {
// only an address is specified - use an empty '<name>=<value>' token array
// Only an address is specified - use an empty '<name>=<value>' token array.
nameValuePairTokens = new String[] {};
} else {
if (addressSplitTokens.length == 2) {
// split into '<name>=<value>' tokens
// Split into '<name>=<value>' tokens.
nameValuePairTokens = addressSplitTokens[1].split("&");
} else {
throw new BitcoinURIParseException("Too many question marks in URI '" + input + "'");
}
}
// Attempt to parse the rest of the URI parameters
parseParameters(networkParameters, addressToken, nameValuePairTokens);
// Attempt to parse the rest of the URI parameters.
parseParameters(params, addressToken, nameValuePairTokens);
}
/**
* @param networkParameters
* @param params
* The network parameters
* @param nameValuePairTokens
* The tokens representing the name value pairs (assumed to be
* separated by '=' e.g. 'amount=0.2')
*/
private void parseParameters(NetworkParameters networkParameters, String addressToken, String[] nameValuePairTokens) {
private void parseParameters(NetworkParameters params, String addressToken, String[] nameValuePairTokens) {
// Attempt to parse the addressToken as a Bitcoin address for this network
try {
Address address = new Address(networkParameters, addressToken);
Address address = new Address(params, addressToken);
putWithValidation(FIELD_ADDRESS, address);
} catch (final AddressFormatException e) {
throw new BitcoinURIParseException("Bad address", e);
}
// Attempt to decode the rest of the tokens into a parameter map
// Attempt to decode the rest of the tokens into a parameter map.
for (int i = 0; i < nameValuePairTokens.length; i++) {
String[] tokens = nameValuePairTokens[i].split("=");
if (tokens.length != 2 || "".equals(tokens[0])) {
throw new BitcoinURIParseException("Malformed Bitcoin URI - cannot parse name value pair '" + nameValuePairTokens[i] + "'");
throw new BitcoinURIParseException("Malformed Bitcoin URI - cannot parse name value pair '" +
nameValuePairTokens[i] + "'");
}
String nameToken = tokens[0].toLowerCase();
String valueToken = tokens[1];
// Parse the amount
// Parse the amount.
if (FIELD_AMOUNT.equals(nameToken)) {
// Decode the amount (contains an optional decimal component to 8dp)
// Decode the amount (contains an optional decimal component to 8dp).
try {
BigInteger amount = Utils.toNanoCoins(valueToken);
putWithValidation(FIELD_AMOUNT, amount);
@@ -208,33 +207,29 @@ public class BitcoinURI {
}
} else {
if (nameToken.startsWith("req-")) {
// A required parameter that we do not know about
// A required parameter that we do not know about.
throw new RequiredFieldValidationException("'" + nameToken + "' is required but not known, this URI is not valid");
} else {
// Known fields and unknown parameters that are optional
// Known fields and unknown parameters that are optional.
try {
putWithValidation(nameToken, URLDecoder.decode(valueToken, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// should not happen as UTF-8 is valid encoding
// Unreachable.
throw new RuntimeException(e);
}
}
}
}
// Note to the future : when you want to implement 'req-expires' have a look at commit 410a53791841 which had it in
// Note to the future: when you want to implement 'req-expires' have a look at commit 410a53791841
// which had it in.
}
/**
* <p>
* Put the value against the key in the map checking for duplication.
* This avoids address field overwrite etc.
* </p>
* Put the value against the key in the map checking for duplication. This avoids address field overwrite etc.
*
* @param key
* The key for the map
* @param value
* The value to store
* @param key The key for the map
* @param value The value to store
*/
private void putWithValidation(String key, Object value) {
if (parameterMap.containsKey(key)) {
@@ -298,18 +293,12 @@ public class BitcoinURI {
}
/**
* <p>
* Simple Bitcoin URI builder using known good fields
* </p>
* Simple Bitcoin URI builder using known good fields.
*
* @param address
* The Bitcoin address
* @param amount
* The amount in nanocoins (decimal)
* @param label
* A label
* @param message
* A message
* @param address The Bitcoin address
* @param amount The amount in nanocoins (decimal)
* @param label A label
* @param message A message
* @return A String containing the Bitcoin URI
*/
public static String convertToBitcoinURI(Address address, BigInteger amount, String label, String message) {
@@ -356,12 +345,9 @@ public class BitcoinURI {
}
/**
* <p>
* Encode a string using URL encoding
* </p>
*
* @param stringToEncode
* The string to URL encode
* @param stringToEncode The string to URL encode
*/
static String encodeURLString(String stringToEncode) {
try {

View File

@@ -9,11 +9,9 @@ package com.google.bitcoin.uri;
* that reported in the exception message). Since this is in English, it may not be worth reporting directly
* to the user other than as part of a "general failure to parse" response.</p>
*
* @since 0.3.0
*  
* @since 0.4.0
*/
public class BitcoinURIParseException extends RuntimeException {
public BitcoinURIParseException(String s) {
super(s);
}

View File

@@ -19,14 +19,16 @@ package com.google.bitcoin.core;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import static org.junit.Assert.*;
public class AddressTest {
static final NetworkParameters testParams = NetworkParameters.testNet();
static final NetworkParameters prodParams = NetworkParameters.prodNet();
@Test
public void testStringification() throws Exception {
public void stringification() throws Exception {
// Test a testnet address.
Address a = new Address(testParams, Hex.decode("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc"));
assertEquals("n4eA2nbYqErp7H6jebchxAN59DmNpksexv", a.toString());
@@ -36,11 +38,54 @@ public class AddressTest {
}
@Test
public void testDecoding() throws Exception {
public void decoding() throws Exception {
Address a = new Address(testParams, "n4eA2nbYqErp7H6jebchxAN59DmNpksexv");
assertEquals("fda79a24e50ff70ff42f7d89585da5bd19d9e5cc", Utils.bytesToHexString(a.getHash160()));
Address b = new Address(prodParams, "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL");
assertEquals("4a22c3c4cbb31e4d03b15550636762bda0baf85a", Utils.bytesToHexString(b.getHash160()));
}
@Test
public void errorPaths() {
// Check what happens if we try and decode garbage.
try {
new Address(testParams, "this is not a valid address!");
fail();
} catch (WrongNetworkException e) {
fail();
} catch (AddressFormatException e) {
// Success.
}
// Check the empty case.
try {
new Address(testParams, "");
fail();
} catch (WrongNetworkException e) {
fail();
} catch (AddressFormatException e) {
// Success.
}
// Check the case of a mismatched network.
try {
new Address(testParams, "17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL");
fail();
} catch (WrongNetworkException e) {
// Success.
assertEquals(e.verCode, NetworkParameters.prodNet().addressHeader);
assertTrue(Arrays.equals(e.acceptableVersions, NetworkParameters.testNet().acceptableAddressCodes));
} catch (AddressFormatException e) {
fail();
}
}
@Test
public void getNetwork() throws Exception {
NetworkParameters params = Address.getParametersFromAddress("17kzeh4N8g49GFvdDzSf8PjaPfyoD1MndL");
assertEquals(NetworkParameters.prodNet().getId(), params.getId());
params = Address.getParametersFromAddress("n4eA2nbYqErp7H6jebchxAN59DmNpksexv");
assertEquals(NetworkParameters.testNet().getId(), params.getId());
}
}

View File

@@ -43,7 +43,7 @@ public class BitcoinURITest {
* @throws AddressFormatException
*/
@Test
public void testConvertToBitcoinURI() throws BitcoinURIParseException, AddressFormatException {
public void testConvertToBitcoinURI() throws Exception {
Address goodAddress = new Address(NetworkParameters.prodNet(), PRODNET_GOOD_ADDRESS);
// simple example