From 6679f42f4a606849057544582c9c0fb8b9e2a322 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 29 Jul 2014 18:13:52 +0200 Subject: [PATCH] Use a local Bitcoin node if one is detected instead of the p2p network. This allows any user of a bitcoinj based app to upgrade from SPV to full mode security by just installing and running Core on the same machine. Can be controlled with a new property on PeerGroup. --- .../com/google/bitcoin/core/PeerAddress.java | 6 +- .../com/google/bitcoin/core/PeerGroup.java | 87 +++++++++++++++++-- .../google/bitcoin/core/PeerGroupTest.java | 20 +++++ .../com/google/bitcoin/tools/WalletTool.java | 10 +-- 4 files changed, 105 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/com/google/bitcoin/core/PeerAddress.java b/core/src/main/java/com/google/bitcoin/core/PeerAddress.java index 79b24b85..d6cbcecd 100644 --- a/core/src/main/java/com/google/bitcoin/core/PeerAddress.java +++ b/core/src/main/java/com/google/bitcoin/core/PeerAddress.java @@ -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 diff --git a/core/src/main/java/com/google/bitcoin/core/PeerGroup.java b/core/src/main/java/com/google/bitcoin/core/PeerGroup.java index 448b642a..2e6b9163 100644 --- a/core/src/main/java/com/google/bitcoin/core/PeerGroup.java +++ b/core/src/main/java/com/google/bitcoin/core/PeerGroup.java @@ -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(); + } + } } diff --git a/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java b/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java index a2060625..65fa37e1 100644 --- a/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java +++ b/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java @@ -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(); + } + } } diff --git a/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java b/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java index 342d2974..f358531c 100644 --- a/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java +++ b/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java @@ -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)); - } + //} } }