This commit is contained in:
Carlos Lopez-Camey
2014-07-29 12:23:42 -06:00
8 changed files with 138 additions and 54 deletions

View File

@@ -100,11 +100,7 @@ public class PeerAddress extends ChildMessage {
}
public static PeerAddress localhost(NetworkParameters params) {
try {
return new PeerAddress(InetAddress.getLocalHost(), params.getPort());
} catch (UnknownHostException e) {
throw new RuntimeException(e); // Broken system.
}
return new PeerAddress(InetAddress.getLoopbackAddress(), params.getPort());
}
@Override

View File

@@ -33,6 +33,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import com.google.common.util.concurrent.*;
@@ -42,8 +43,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
@@ -119,6 +122,8 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
public static final long DEFAULT_PING_INTERVAL_MSEC = 2000;
private long pingIntervalMsec = DEFAULT_PING_INTERVAL_MSEC;
@GuardedBy("lock") private boolean useLocalhostPeerWhenPossible = true;
private final NetworkParameters params;
private final AbstractBlockChain chain;
@GuardedBy("lock") private long fastCatchupTimeSecs;
@@ -236,7 +241,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
}
}
// Visible for testing
@VisibleForTesting
PeerEventListener startupListener = new PeerStartupListener();
/**
@@ -687,6 +692,32 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
}
}
private enum LocalhostCheckState {
NOT_TRIED,
FOUND,
FOUND_AND_CONNECTED,
NOT_THERE
}
private LocalhostCheckState localhostCheckState = LocalhostCheckState.NOT_TRIED;
private boolean maybeCheckForLocalhostPeer() {
checkState(lock.isHeldByCurrentThread());
if (localhostCheckState == LocalhostCheckState.NOT_TRIED) {
// Do a fast blocking connect to see if anything is listening.
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), params.getPort()), vConnectTimeoutMillis);
localhostCheckState = LocalhostCheckState.FOUND;
Closeables.close(socket, true);
return true;
} catch (IOException e) {
log.info("Localhost peer not detected.");
localhostCheckState = LocalhostCheckState.NOT_THERE;
}
}
return false;
}
/** Picks a peer from discovery and connects to it. If connection fails, picks another and tries again. */
protected void connectToAnyPeer() throws PeerDiscoveryException {
final State state = state();
@@ -698,6 +729,12 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
long retryTime = 0;
lock.lock();
try {
if (useLocalhostPeerWhenPossible && maybeCheckForLocalhostPeer()) {
log.info("Localhost peer detected, trying to use it instead of P2P discovery");
maxConnections = 0;
connectToLocalHost();
return;
}
if (!haveReadyInactivePeer(nowMillis)) {
discoverPeers();
groupBackoff.trackSuccess();
@@ -716,14 +753,14 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
if (retryTime > nowMillis) {
// Sleep until retry time
final long millis = retryTime - nowMillis;
log.info("Waiting {} msec before next connect attempt {}", millis, addr == null ? "" : " to " + addr);
log.info("Waiting {} msec before next connect attempt {}", millis, addr == null ? "" : "to " + addr);
Utils.sleep(millis);
}
}
// This method constructs a Peer and puts it into pendingPeers.
checkNotNull(addr); // Help static analysis which can't see that addr is always set if we didn't throw above.
connectTo(addr, false);
connectTo(addr, false, vConnectTimeoutMillis);
}
private boolean haveReadyInactivePeer(long nowMillis) {
@@ -931,7 +968,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
public Peer connectTo(InetSocketAddress address) {
PeerAddress peerAddress = new PeerAddress(address);
backoffMap.put(peerAddress, new ExponentialBackoff(peerBackoffParams));
return connectTo(peerAddress, true);
return connectTo(peerAddress, true, vConnectTimeoutMillis);
}
/**
@@ -941,12 +978,19 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
public Peer connectToLocalHost() {
final PeerAddress localhost = PeerAddress.localhost(params);
backoffMap.put(localhost, new ExponentialBackoff(peerBackoffParams));
return connectTo(localhost, true);
return connectTo(localhost, true, vConnectTimeoutMillis);
}
// Internal version.
/**
* Creates a version message to send, constructs a Peer object and attempts to connect it. Returns the peer on
* success or null on failure.
* @param address Remote network address
* @param incrementMaxConnections Whether to consider this connection an attempt to fill our quota, or something
* explicitly requested.
* @return Peer or null.
*/
@Nullable
protected Peer connectTo(PeerAddress address, boolean incrementMaxConnections) {
protected Peer connectTo(PeerAddress address, boolean incrementMaxConnections, int connectTimeoutMillis) {
VersionMessage ver = getVersionMessage().duplicate();
ver.bestHeight = chain == null ? 0 : chain.getBestChainHeight();
ver.time = Utils.currentTimeSeconds();
@@ -963,7 +1007,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
handlePeerDeath(peer);
return null;
}
peer.setSocketTimeout(vConnectTimeoutMillis);
peer.setSocketTimeout(connectTimeoutMillis);
// When the channel has connected and version negotiated successfully, handleNewPeer will end up being called on
// a worker thread.
@@ -1576,8 +1620,35 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
}
}
/**
* Returns the {@link com.subgraph.orchid.TorClient} object for this peer group, if Tor is in use, null otherwise.
*/
@Nullable
public TorClient getTorClient() {
return torClient;
}
/** See {@link #setUseLocalhostPeerWhenPossible(boolean)} */
public boolean getUseLocalhostPeerWhenPossible() {
lock.lock();
try {
return useLocalhostPeerWhenPossible;
} finally {
lock.unlock();
}
}
/**
* When true (the default), PeerGroup will attempt to connect to a Bitcoin node running on localhost before
* attempting to use the P2P network. If successful, only localhost will be used. This makes for a simple
* and easy way for a user to upgrade a bitcoinj based app running in SPV mode to fully validating security.
*/
public void setUseLocalhostPeerWhenPossible(boolean useLocalhostPeerWhenPossible) {
lock.lock();
try {
this.useLocalhostPeerWhenPossible = useLocalhostPeerWhenPossible;
} finally {
lock.unlock();
}
}
}

View File

@@ -548,6 +548,11 @@ public class Utils {
}
}
public static boolean isAndroidRuntime() {
final String runtime = System.getProperty("java.runtime.name");
return runtime != null && runtime.equals("Android Runtime");
}
private static class Pair implements Comparable<Pair> {
int item, count;
public Pair(int item, int count) { this.count = count; this.item = item; }

View File

@@ -1,5 +1,6 @@
package com.google.bitcoin.crypto;
import com.google.bitcoin.core.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -10,11 +11,6 @@ import java.lang.reflect.Method;
public class DRMWorkaround {
private static Logger log = LoggerFactory.getLogger(DRMWorkaround.class);
public static boolean isAndroidRuntime() {
final String runtime = System.getProperty("java.runtime.name");
return runtime != null && runtime.equals("Android Runtime");
}
private static boolean done = false;
public static void maybeDisableExportControls() {
@@ -26,7 +22,7 @@ public class DRMWorkaround {
if (done) return;
done = true;
if (isAndroidRuntime())
if (Utils.isAndroidRuntime())
return;
try {
Field gate = Class.forName("javax.crypto.JceSecurity").getDeclaredField("isRestricted");

View File

@@ -18,7 +18,10 @@
package com.google.bitcoin.crypto;
import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.core.Utils;
import com.google.common.base.Joiner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
@@ -39,6 +42,8 @@ import static com.google.bitcoin.core.Utils.HEX;
*/
public class MnemonicCode {
private static final Logger log = LoggerFactory.getLogger(MnemonicCode.class);
private ArrayList<String> wordList;
public static String BIP39_ENGLISH_SHA256 = "ad90bf3beb7b0eb7e5acd74727dc0da96e0a280a258354e7293fb7e211ac03db";
@@ -48,6 +53,19 @@ public class MnemonicCode {
private static final int PBKDF2_ROUNDS = 2048;
public static MnemonicCode INSTANCE;
static {
try {
INSTANCE = new MnemonicCode();
} catch (IOException e) {
// We expect failure on Android. The developer has to set INSTANCE themselves.
if (!Utils.isAndroidRuntime())
log.error("Failed to load word list", e);
}
}
/** Initialise from the included word list. Won't work on Android. */
public MnemonicCode() throws IOException {
this(MnemonicCode.class.getResourceAsStream("mnemonic/wordlist/english.txt"), BIP39_ENGLISH_SHA256);
}

View File

@@ -19,15 +19,14 @@ package com.google.bitcoin.wallet;
import com.google.bitcoin.crypto.*;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import org.bitcoinj.wallet.Protos;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.List;
@@ -42,31 +41,15 @@ import static com.google.common.base.Preconditions.checkState;
* code.
*/
public class DeterministicSeed implements EncryptableItem {
// It would take more than 10^12 years to brute-force a 128 bit seed using $1B worth
// of computing equipment.
// It would take more than 10^12 years to brute-force a 128 bit seed using $1B worth of computing equipment.
public static final int DEFAULT_SEED_ENTROPY_BITS = 128;
public static final int MAX_SEED_ENTROPY_BITS = 512;
public static final String UTF_8 = "UTF-8";
@Nullable private final byte[] seed;
@Nullable private List<String> mnemonicCode;
@Nullable private EncryptedData encryptedMnemonicCode;
private final long creationTimeSeconds;
private static MnemonicCode MNEMONIC_CODEC;
private static synchronized MnemonicCode getCachedMnemonicCodec() {
try {
// This object can be large and has to load the word list from disk, so we lazy cache it.
if (MNEMONIC_CODEC == null) {
MNEMONIC_CODEC = new MnemonicCode();
}
return MNEMONIC_CODEC;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
DeterministicSeed(String mnemonicCode, String passphrase, long creationTimeSeconds) throws UnreadableWalletException {
this(decodeMnemonicCode(mnemonicCode), passphrase, creationTimeSeconds);
}
@@ -120,7 +103,7 @@ public class DeterministicSeed implements EncryptableItem {
Preconditions.checkArgument(entropy.length * 8 >= DEFAULT_SEED_ENTROPY_BITS, "entropy size too small");
try {
this.mnemonicCode = getCachedMnemonicCodec().toMnemonic(entropy);
this.mnemonicCode = MnemonicCode.INSTANCE.toMnemonic(entropy);
} catch (MnemonicException.MnemonicLengthException e) {
// cannot happen
throw new RuntimeException(e);
@@ -168,6 +151,7 @@ public class DeterministicSeed implements EncryptableItem {
return getMnemonicAsBytes();
}
@Nullable
public byte[] getSeedBytes() {
return seed;
}
@@ -196,11 +180,7 @@ public class DeterministicSeed implements EncryptableItem {
}
private byte[] getMnemonicAsBytes() {
try {
return Joiner.on(" ").join(mnemonicCode).getBytes(UTF_8);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return Joiner.on(" ").join(mnemonicCode).getBytes(Charsets.UTF_8);
}
public DeterministicSeed decrypt(KeyCrypter crypter, String passphrase, KeyParameter aesKey) {
@@ -249,11 +229,11 @@ public class DeterministicSeed implements EncryptableItem {
*/
public void check() throws MnemonicException {
if (mnemonicCode != null)
getCachedMnemonicCodec().check(mnemonicCode);
MnemonicCode.INSTANCE.check(mnemonicCode);
}
byte[] getEntropyBytes() throws MnemonicException {
return getCachedMnemonicCodec().toEntropy(mnemonicCode);
return MnemonicCode.INSTANCE.toEntropy(mnemonicCode);
}
/** Get the mnemonic code, or null if unknown. */
@@ -263,13 +243,11 @@ public class DeterministicSeed implements EncryptableItem {
}
private static List<String> decodeMnemonicCode(byte[] mnemonicCode) throws UnreadableWalletException {
String code = null;
try {
code = new String(mnemonicCode, "UTF-8");
return Splitter.on(" ").splitToList(new String(mnemonicCode, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new UnreadableWalletException(e.toString());
}
return Splitter.on(" ").splitToList(code);
}
private static List<String> decodeMnemonicCode(String mnemonicCode) {

View File

@@ -34,7 +34,10 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
@@ -645,4 +648,21 @@ public class PeerGroupTest extends TestWithPeerGroup {
future.get();
assertTrue(future.isDone());
}
@Test
public void preferLocalPeer() throws IOException {
// Check that if we have a localhost port 8333 then it's used instead of the p2p network.
ServerSocket local = new ServerSocket(params.getPort(), 100, InetAddress.getLoopbackAddress());
try {
peerGroup.startAsync();
peerGroup.awaitRunning();
local.accept().close(); // Probe connect
local.accept(); // Real connect
// If we get here it used the local peer. Check no others are in use.
assertEquals(1, peerGroup.getMaxConnections());
assertEquals(PeerAddress.localhost(params), peerGroup.getPendingPeers().get(0).getAddress());
} finally {
local.close();
}
}
}

View File

@@ -823,12 +823,12 @@ public class WalletTool {
}
} else if (!options.has("tor")) {
// If Tor mode then PeerGroup already has discovery set up.
if (params == RegTestParams.get()) {
log.info("Assuming regtest node on localhost");
peers.addAddress(PeerAddress.localhost(params));
} else {
// if (params == RegTestParams.get()) {
// log.info("Assuming regtest node on localhost");
// peers.addAddress(PeerAddress.localhost(params));
// } else {
peers.addPeerDiscovery(new DnsDiscovery(params));
}
//}
}
}