/** * Copyright 2013 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.devcoin.core; import com.google.devcoin.core.Peer.PeerHandler; import com.google.devcoin.discovery.PeerDiscovery; import com.google.devcoin.discovery.PeerDiscoveryException; import com.google.devcoin.utils.ListenerRegistration; import com.google.devcoin.utils.Threading; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import com.google.common.util.concurrent.*; import net.jcip.annotations.GuardedBy; import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.channel.*; import org.jboss.netty.channel.group.ChannelGroup; import org.jboss.netty.channel.group.DefaultChannelGroup; import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.IOException; import java.math.BigInteger; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; /** * <p>Runs a set of connections to the P2P network, brings up connections to replace disconnected nodes and manages * the interaction between them all. Most applications will want to use one of these.</p> * * <p>PeerGroup tries to maintain a constant number of connections to a set of distinct peers. * Each peer runs a network listener in its own thread. When a connection is lost, a new peer * will be tried after a delay as long as the number of connections less than the maximum.</p> * * <p>Connections are made to addresses from a provided list. When that list is exhausted, * we start again from the head of the list.</p> * * <p>The PeerGroup can broadcast a transaction to the currently connected set of peers. It can * also handle download of the blockchain from peers, restarting the process when peers die.</p> * * <p>PeerGroup implements the {@link Service} interface. This means before it will do anything, * you must call the {@link com.google.common.util.concurrent.Service#start()} method (which returns * a future) or {@link com.google.common.util.concurrent.Service#startAndWait()} method, which will block * until peer discovery is completed and some outbound connections have been initiated (it will return * before handshaking is done, however). You should call {@link com.google.common.util.concurrent.Service#stop()} * when finished. Note that not all methods of PeerGroup are safe to call from a UI thread as some may do * network IO, but starting and stopping the service should be fine.</p> */ public class PeerGroup extends AbstractIdleService implements TransactionBroadcaster { private static final int DEFAULT_CONNECTIONS = 4; private static final Logger log = LoggerFactory.getLogger(PeerGroup.class); protected final ReentrantLock lock = Threading.lock("peergroup"); // Addresses to try to connect to, excluding active peers. @GuardedBy("lock") private final List<PeerAddress> inactives; // Currently active peers. This is an ordered list rather than a set to make unit tests predictable. private final CopyOnWriteArrayList<Peer> peers; // Currently connecting peers. private final CopyOnWriteArrayList<Peer> pendingPeers; private final ChannelGroup channels; // The peer that has been selected for the purposes of downloading announced data. @GuardedBy("lock") private Peer downloadPeer; // Callback for events related to chain download @GuardedBy("lock") private PeerEventListener downloadListener; // Callbacks for events related to peer connection/disconnection private final CopyOnWriteArrayList<ListenerRegistration<PeerEventListener>> peerEventListeners; // Peer discovery sources, will be polled occasionally if there aren't enough inactives. private final CopyOnWriteArraySet<PeerDiscovery> peerDiscoverers; // The version message to use for new connections. @GuardedBy("lock") private VersionMessage versionMessage; // A class that tracks recent transactions that have been broadcast across the network, counts how many // peers announced them and updates the transaction confidence data. It is passed to each Peer. private final MemoryPool memoryPool; // How many connections we want to have open at the current time. If we lose connections, we'll try opening more // until we reach this count. @GuardedBy("lock") private int maxConnections; // Minimum protocol version we will allow ourselves to connect to: require Bloom filtering. private volatile int vMinRequiredProtocolVersion = FilteredBlock.MIN_PROTOCOL_VERSION; // Runs a background thread that we use for scheduling pings to our peers, so we can measure their performance // and network latency. We ping peers every pingIntervalMsec milliseconds. private volatile Timer vPingTimer; /** How many milliseconds to wait after receiving a pong before sending another ping. */ public static final long DEFAULT_PING_INTERVAL_MSEC = 2000; private long pingIntervalMsec = DEFAULT_PING_INTERVAL_MSEC; private final NetworkParameters params; private final AbstractBlockChain chain; @GuardedBy("lock") private long fastCatchupTimeSecs; private final CopyOnWriteArrayList<Wallet> wallets; private final CopyOnWriteArrayList<PeerFilterProvider> peerFilterProviders; // This event listener is added to every peer. It's here so when we announce transactions via an "inv", every // peer can fetch them. private final AbstractPeerEventListener getDataListener = new AbstractPeerEventListener() { @Override public List<Message> getData(Peer peer, GetDataMessage m) { return handleGetData(m); } }; private ClientBootstrap bootstrap; private int minBroadcastConnections = 0; private AbstractWalletEventListener walletEventListener = new AbstractWalletEventListener() { private void onChanged() { recalculateFastCatchupAndFilter(); } @Override public void onKeysAdded(Wallet wallet, List<ECKey> keys) { onChanged(); } @Override public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { onChanged(); } @Override public void onCoinsSent(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { onChanged(); } }; private class PeerStartupListener implements Peer.PeerLifecycleListener { public void onPeerConnected(Peer peer) { handleNewPeer(peer); } public void onPeerDisconnected(Peer peer) { // The channel will be automatically removed from channels. handlePeerDeath(peer); } } // Visible for testing Peer.PeerLifecycleListener startupListener = new PeerStartupListener(); // A bloom filter generated from all connected wallets that is given to new peers private BloomFilter bloomFilter; /** A reasonable default for the bloom filter false positive rate on mainnet. * Users for which low data usage is of utmost concern, 0.0001 may be better, for users * to whom anonymity is of utmost concern, 0.001 should provide very good privacy */ public static final double DEFAULT_BLOOM_FILTER_FP_RATE = 0.0005; // The false positive rate for bloomFilter private double bloomFilterFPRate = DEFAULT_BLOOM_FILTER_FP_RATE; // We use a constant tweak to avoid giving up privacy when we regenerate our filter with new keys private final long bloomFilterTweak = (long) (Math.random() * Long.MAX_VALUE); private int lastBloomFilterElementCount; /** * Creates a PeerGroup with the given parameters. No chain is provided so this node will report its chain height * as zero to other peers. This constructor is useful if you just want to explore the network but aren't interested * in downloading block data. * * @param params Network parameters */ public PeerGroup(NetworkParameters params) { this(params, null); } /** * Creates a PeerGroup for the given network and chain. Blocks will be passed to the chain as they are broadcast * and downloaded. This is probably the constructor you want to use. */ public PeerGroup(NetworkParameters params, AbstractBlockChain chain) { this(params, chain, null); } /** * <p>Creates a PeerGroup for the given network and chain, using the provided Netty {@link ClientBootstrap} object. * </p> * * <p>A ClientBootstrap creates raw (TCP) connections to other nodes on the network. Normally you won't need to * provide one - use the other constructors. Providing your own bootstrap is useful if you want to control * details like how many network threads are used, the connection timeout value and so on. To do this, you can * use {@link PeerGroup#createClientBootstrap()} method and then customize the resulting object. Example:</p> * * <pre> * ClientBootstrap bootstrap = PeerGroup.createClientBootstrap(); * bootstrap.setOption("connectTimeoutMillis", 3000); * PeerGroup peerGroup = new PeerGroup(params, chain, bootstrap); * </pre> * * <p>The ClientBootstrap provided does not need a channel pipeline factory set. If one wasn't set, the provided * bootstrap will be modified to have one that sets up the pipelines correctly.</p> */ public PeerGroup(NetworkParameters params, @Nullable AbstractBlockChain chain, @Nullable ClientBootstrap bootstrap) { this.params = checkNotNull(params); this.chain = chain; this.fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds(); this.wallets = new CopyOnWriteArrayList<Wallet>(); this.peerFilterProviders = new CopyOnWriteArrayList<PeerFilterProvider>(); // This default sentinel value will be overridden by one of two actions: // - adding a peer discovery source sets it to the default // - using connectTo() will increment it by one this.maxConnections = 0; int height = chain == null ? 0 : chain.getBestChainHeight(); // We never request that the remote node wait for a bloom filter yet, as we have no wallets this.versionMessage = new VersionMessage(params, height, true); memoryPool = new MemoryPool(); // Configure Netty. The "ClientBootstrap" creates connections to other nodes. It can be configured in various // ways to control the network. if (bootstrap == null) { this.bootstrap = createClientBootstrap(); this.bootstrap.setPipelineFactory(makePipelineFactory(params, chain)); } else { this.bootstrap = bootstrap; } inactives = new ArrayList<PeerAddress>(); peers = new CopyOnWriteArrayList<Peer>(); pendingPeers = new CopyOnWriteArrayList<Peer>(); channels = new DefaultChannelGroup(); peerDiscoverers = new CopyOnWriteArraySet<PeerDiscovery>(); peerEventListeners = new CopyOnWriteArrayList<ListenerRegistration<PeerEventListener>>(); } /** * Helper method that just sets up a normal Netty ClientBootstrap using the default options, except for a custom * thread factory that gives worker threads useful names and lowers their priority (to avoid competing with UI * threads). You don't normally need to call this - if you aren't sure what it does, just use the regular * constructors for {@link PeerGroup} that don't take a ClientBootstrap object. */ public static ClientBootstrap createClientBootstrap() { ExecutorService bossExecutor = Executors.newCachedThreadPool(new PeerGroupThreadFactory()); ExecutorService workerExecutor = Executors.newCachedThreadPool(new PeerGroupThreadFactory()); NioClientSocketChannelFactory channelFactory = new NioClientSocketChannelFactory(bossExecutor, workerExecutor); ClientBootstrap bs = new ClientBootstrap(channelFactory); bs.setOption("connectTimeoutMillis", 2000); return bs; } // Create a Netty pipeline factory. The pipeline factory will create a network processing // pipeline with the bitcoin serializer ({@code TCPNetworkConnection}) downstream // of the higher level {@code Peer}. Received packets will first be decoded, then passed // {@code Peer}. Sent packets will be created by the {@code Peer}, then encoded and sent. private ChannelPipelineFactory makePipelineFactory(final NetworkParameters params, @Nullable final AbstractBlockChain chain) { return new ChannelPipelineFactory() { public ChannelPipeline getPipeline() throws Exception { // This runs unlocked. VersionMessage ver = getVersionMessage().duplicate(); ver.bestHeight = chain == null ? 0 : chain.getBestChainHeight(); ver.time = Utils.now().getTime() / 1000; ChannelPipeline p = Channels.pipeline(); Peer peer = new Peer(params, chain, ver, memoryPool); peer.addLifecycleListener(startupListener); peer.setMinProtocolVersion(vMinRequiredProtocolVersion); pendingPeers.add(peer); TCPNetworkConnection codec = new TCPNetworkConnection(params, peer.getVersionMessage()); p.addLast("codec", codec.getHandler()); p.addLast("peer", peer.getHandler()); return p; } }; } /** * Adjusts the desired number of connections that we will create to peers. Note that if there are already peers * open and the new value is lower than the current number of peers, those connections will be terminated. Likewise * if there aren't enough current connections to meet the new requested max size, some will be added. */ public void setMaxConnections(int maxConnections) { int adjustment; lock.lock(); try { this.maxConnections = maxConnections; if (!isRunning()) return; } finally { lock.unlock(); } // We may now have too many or too few open connections. Add more or drop some to get to the right amount. adjustment = maxConnections - channels.size(); while (adjustment > 0) { try { connectToAnyPeer(); } catch (PeerDiscoveryException e) { throw new RuntimeException(e); } adjustment--; } while (adjustment < 0) { channels.iterator().next().close(); adjustment++; } } /** The maximum number of connections that we will create to peers. */ public int getMaxConnections() { lock.lock(); try { return maxConnections; } finally { lock.unlock(); } } private List<Message> handleGetData(GetDataMessage m) { // Scans the wallets and memory pool for transactions in the getdata message and returns them. // Runs on peer threads. lock.lock(); try { LinkedList<Message> transactions = new LinkedList<Message>(); LinkedList<InventoryItem> items = new LinkedList<InventoryItem>(m.getItems()); Iterator<InventoryItem> it = items.iterator(); while (it.hasNext()) { InventoryItem item = it.next(); // Check the mempool first. Transaction tx = memoryPool.get(item.hash); if (tx != null) { transactions.add(tx); it.remove(); } else { // Check the wallets. for (Wallet w : wallets) { tx = w.getTransaction(item.hash); if (tx == null) continue; transactions.add(tx); it.remove(); break; } } } return transactions; } finally { lock.unlock(); } } /** * Sets the {@link VersionMessage} that will be announced on newly created connections. A version message is * primarily interesting because it lets you customize the "subVer" field which is used a bit like the User-Agent * field from HTTP. It means your client tells the other side what it is, see * <a href="https://en.bitcoin.it/wiki/BIP_0014">BIP 14</a>. * * The VersionMessage you provide is copied and the best chain height/time filled in for each new connection, * therefore you don't have to worry about setting that. The provided object is really more of a template. */ public void setVersionMessage(VersionMessage ver) { lock.lock(); try { versionMessage = ver; } finally { lock.unlock(); } } /** * Returns the version message provided by setVersionMessage or a default if none was given. */ public VersionMessage getVersionMessage() { lock.lock(); try { return versionMessage; } finally { lock.unlock(); } } /** * Sets information that identifies this software to remote nodes. This is a convenience wrapper for creating * a new {@link VersionMessage}, calling {@link VersionMessage#appendToSubVer(String, String, String)} on it, * and then calling {@link PeerGroup#setVersionMessage(VersionMessage)} on the result of that. See the docs for * {@link VersionMessage#appendToSubVer(String, String, String)} for information on what the fields should contain. * * @param name * @param version */ public void setUserAgent(String name, String version, String comments) { //TODO Check that height is needed here (it wasnt, but it should be, no?) int height = chain == null ? 0 : chain.getBestChainHeight(); VersionMessage ver = new VersionMessage(params, height, false); updateVersionMessageRelayTxesBeforeFilter(ver); ver.appendToSubVer(name, version, comments); setVersionMessage(ver); } // Updates the relayTxesBeforeFilter flag of ver private void updateVersionMessageRelayTxesBeforeFilter(VersionMessage ver) { // We will provide the remote node with a bloom filter (ie they shouldn't relay yet) // iff chain == null || !chain.shouldVerifyTransactions() and a wallet is added // Note that the default here means that no tx invs will be received if no wallet is ever added lock.lock(); try { boolean spvMode = chain != null && !chain.shouldVerifyTransactions(); boolean willSendFilter = spvMode && peerFilterProviders.size() > 0; ver.relayTxesBeforeFilter = !willSendFilter; } finally { lock.unlock(); } } /** * Sets information that identifies this software to remote nodes. This is a convenience wrapper for creating * a new {@link VersionMessage}, calling {@link VersionMessage#appendToSubVer(String, String, String)} on it, * and then calling {@link PeerGroup#setVersionMessage(VersionMessage)} on the result of that. See the docs for * {@link VersionMessage#appendToSubVer(String, String, String)} for information on what the fields should contain. * * @param name * @param version */ public void setUserAgent(String name, String version) { setUserAgent(name, version, null); } /** * <p>Adds a listener that will be notified on the given executor when:</p> * <ol> * <li>New peers are connected to.</li> * <li>Peers are disconnected from.</li> * <li>A message is received by the download peer (there is always one peer which is elected as a peer which * will be used to retrieve data). * <li>Blocks are downloaded by the download peer.</li> * </li> * </ol> */ public void addEventListener(PeerEventListener listener, Executor executor) { peerEventListeners.add(new ListenerRegistration<PeerEventListener>(checkNotNull(listener), executor)); } /** * Same as {@link PeerGroup#addEventListener(PeerEventListener, java.util.concurrent.Executor)} but defaults * to running on the user thread. */ public void addEventListener(PeerEventListener listener) { addEventListener(listener, Threading.USER_THREAD); } /** The given event listener will no longer be called with events. */ public boolean removeEventListener(PeerEventListener listener) { return ListenerRegistration.removeFromList(listener, peerEventListeners); } /** * Removes all event listeners simultaneously. Note that this includes listeners added internally by the framework * so it's generally not advised to use this - it exists for special purposes only. */ public void clearEventListeners() { peerEventListeners.clear(); } /** * Returns a newly allocated list containing the currently connected peers. If all you care about is the count, * use numConnectedPeers(). */ public List<Peer> getConnectedPeers() { lock.lock(); try { return new ArrayList<Peer>(peers); } finally { lock.unlock(); } } /** * Returns a list containing Peers that did not complete connection yet. */ public List<Peer> getPendingPeers() { lock.lock(); try { return new ArrayList<Peer>(pendingPeers); } finally { lock.unlock(); } } /** * Add an address to the list of potential peers to connect to. It won't necessarily be used unless there's a need * to build new connections to reach the max connection count. * * @param peerAddress IP/port to use. */ public void addAddress(PeerAddress peerAddress) { int newMax; lock.lock(); try { inactives.add(peerAddress); newMax = getMaxConnections() + 1; } finally { lock.unlock(); } setMaxConnections(newMax); } /** Convenience method for addAddress(new PeerAddress(address, params.port)); */ public void addAddress(InetAddress address) { addAddress(new PeerAddress(address, params.getPort())); } /** * Add addresses from a discovery source to the list of potential peers to connect to. If max connections has not * been configured, or set to zero, then it's set to the default at this point. */ public void addPeerDiscovery(PeerDiscovery peerDiscovery) { lock.lock(); try { if (getMaxConnections() == 0) setMaxConnections(DEFAULT_CONNECTIONS); peerDiscoverers.add(peerDiscovery); } finally { lock.unlock(); } } protected void discoverPeers() throws PeerDiscoveryException { long start = System.currentTimeMillis(); Set<PeerAddress> addressSet = Sets.newHashSet(); for (PeerDiscovery peerDiscovery : peerDiscoverers) { InetSocketAddress[] addresses; addresses = peerDiscovery.getPeers(5, TimeUnit.SECONDS); for (InetSocketAddress address : addresses) addressSet.add(new PeerAddress(address)); if (addressSet.size() > 0) break; } lock.lock(); try { inactives.addAll(addressSet); } finally { lock.unlock(); } log.info("Peer discovery took {}msec", System.currentTimeMillis() - start); } /** 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(); if (!(state == State.STARTING || state == State.RUNNING)) return; final PeerAddress addr; lock.lock(); try { if (inactives.size() == 0) { discoverPeers(); } if (inactives.size() == 0) { log.debug("Peer discovery didn't provide us any more peers, not trying to build new connection."); return; } addr = inactives.remove(inactives.size() - 1); } finally { lock.unlock(); } // Don't do connectTo whilst holding the PeerGroup lock because this can trigger some amazingly deep stacks // and potentially circular deadlock in the case of immediate failure (eg, attempt to access IPv6 node from // a non-v6 capable machine). It doesn't relay control immediately to the netty boss thread as you may expect. // // This method eventually constructs a Peer and puts it into pendingPeers. If the connection fails to establish, // handlePeerDeath will be called, which will potentially call this method again to replace the dead or failed // connection. connectTo(addr.toSocketAddress(), false); } @Override protected void startUp() throws Exception { // This is run in a background thread by the AbstractIdleService implementation. vPingTimer = new Timer("Peer pinging thread", true); // Bring up the requested number of connections. If a connect attempt fails, // new peers will be tried until there is a success, so just calling connectToAnyPeer for the wanted number // of peers is sufficient. for (int i = 0; i < getMaxConnections(); i++) { try { connectToAnyPeer(); } catch (PeerDiscoveryException e) { if (e.getCause() instanceof InterruptedException) return; log.error(e.getMessage()); } } } @Override protected void shutDown() throws Exception { // This is run on a separate thread by the AbstractIdleService implementation. vPingTimer.cancel(); // Blocking close of all sockets. TODO: there is a race condition here, for the solution see: // http://biasedbit.com/netty-releaseexternalresources-hangs/ channels.close().await(); // All thread pools should be stopped by this call. bootstrap.releaseExternalResources(); for (PeerDiscovery peerDiscovery : peerDiscoverers) { peerDiscovery.shutdown(); } } /** * <p>Link the given wallet to this PeerGroup. This is used for three purposes:</p> * * <ol> * <li>So the wallet receives broadcast transactions.</li> * <li>Announcing pending transactions that didn't get into the chain yet to our peers.</li> * <li>Set the fast catchup time using {@link PeerGroup#setFastCatchupTimeSecs(long)}, to optimize chain * download.</li> * </ol> * * <p>Note that this should be done before chain download commences because if you add a wallet with keys earlier * than the current chain head, the relevant parts of the chain won't be redownloaded for you.</p> * * <p>The Wallet will have an event listener registered on it, so to avoid leaks remember to use * {@link PeerGroup#removeWallet(Wallet)} on it if you wish to keep the Wallet but lose the PeerGroup.</p> */ public void addWallet(Wallet wallet) { lock.lock(); try { checkNotNull(wallet); checkState(!wallets.contains(wallet)); wallets.add(wallet); wallet.setTransactionBroadcaster(this); wallet.addEventListener(walletEventListener); // TODO: Run this in the current peer thread. addPeerFilterProvider(wallet); } finally { lock.unlock(); } } /** * <p>Link the given PeerFilterProvider to this PeerGroup. DO NOT use this for Wallets, use * {@link PeerGroup#addWallet(Wallet)} instead.</p> * * <p>Note that this should be done before chain download commences because if you add a listener with keys earlier * than the current chain head, the relevant parts of the chain won't be redownloaded for you.</p> */ public void addPeerFilterProvider(PeerFilterProvider provider) { lock.lock(); try { checkNotNull(provider); checkState(!peerFilterProviders.contains(provider)); peerFilterProviders.add(provider); // Don't bother downloading block bodies before the oldest keys in all our wallets. Make sure we recalculate // if a key is added. Of course, by then we may have downloaded the chain already. Ideally adding keys would // automatically rewind the block chain and redownload the blocks to find transactions relevant to those keys, // all transparently and in the background. But we are a long way from that yet. recalculateFastCatchupAndFilter(); updateVersionMessageRelayTxesBeforeFilter(getVersionMessage()); } finally { lock.unlock(); } } /** * Unlinks the given wallet so it no longer receives broadcast transactions or has its transactions announced. */ public void removeWallet(Wallet wallet) { wallets.remove(checkNotNull(wallet)); peerFilterProviders.remove(wallet); wallet.removeEventListener(walletEventListener); wallet.setTransactionBroadcaster(null); } /** * Recalculates the bloom filter given to peers as well as the timestamp after which full blocks are downloaded * (instead of only headers). */ public void recalculateFastCatchupAndFilter() { lock.lock(); try { // Fully verifying mode doesn't use this optimization (it can't as it needs to see all transactions). if (chain != null && chain.shouldVerifyTransactions()) return; long earliestKeyTimeSecs = Long.MAX_VALUE; int elements = 0; for (PeerFilterProvider p : peerFilterProviders) { earliestKeyTimeSecs = Math.min(earliestKeyTimeSecs, p.getEarliestKeyCreationTime()); elements += p.getBloomFilterElementCount(); } if (elements > 0) { // We stair-step our element count so that we avoid creating a filter with different parameters // as much as possible as that results in a loss of privacy. // The constant 100 here is somewhat arbitrary, but makes sense for small to medium wallets - // it will likely mean we never need to create a filter with different parameters. lastBloomFilterElementCount = elements > lastBloomFilterElementCount ? elements + 100 : lastBloomFilterElementCount; BloomFilter filter = new BloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak); for (PeerFilterProvider p : peerFilterProviders) filter.merge(p.getBloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak)); if (!filter.equals(bloomFilter)) { bloomFilter = filter; for (Peer peer : peers) try { peer.setBloomFilter(filter); } catch (IOException e) { throw new RuntimeException(e); } } } // Now adjust the earliest key time backwards by a week to handle the case of clock drift. This can occur // both in block header timestamps and if the users clock was out of sync when the key was first created // (to within a small amount of tolerance). earliestKeyTimeSecs -= 86400 * 7; // Do this last so that bloomFilter is already set when it gets called. setFastCatchupTimeSecs(earliestKeyTimeSecs); } finally { lock.unlock(); } } /** * Sets the false positive rate of bloom filters given to peers. * Be careful regenerating the bloom filter too often, as it decreases anonymity because remote nodes can * compare transactions against both the new and old filters to significantly decrease the false positive rate. * * See the docs for {@link BloomFilter#BloomFilter(int, double, long, BloomFilter.BloomUpdate)} for a brief * explanation of anonymity when using bloom filters. */ public void setBloomFilterFalsePositiveRate(double bloomFilterFPRate) { lock.lock(); try { this.bloomFilterFPRate = bloomFilterFPRate; recalculateFastCatchupAndFilter(); } finally { lock.unlock(); } } /** * Returns the number of currently connected peers. To be informed when this count changes, register a * {@link PeerEventListener} and use the onPeerConnected/onPeerDisconnected methods. */ public int numConnectedPeers() { return peers.size(); } /** * Connect to a peer by creating a Netty channel to the destination address. * * @param address destination IP and port. * @return a ChannelFuture that can be used to wait for the socket to connect. A socket * connection does not mean that protocol handshake has occured. */ public ChannelFuture connectTo(SocketAddress address) { return connectTo(address, true); } // Internal version. protected ChannelFuture connectTo(SocketAddress address, boolean incrementMaxConnections) { ChannelFuture future = bootstrap.connect(address); // Make sure that the channel group gets access to the channel only if it connects successfully (otherwise // it cannot be closed and trying to do so will cause problems). future.addListener(new ChannelFutureListener() { public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) channels.add(future.getChannel()); } }); // When the channel has connected and version negotiated successfully, handleNewPeer will end up being called on // a worker thread. // Set up the address on the TCPNetworkConnection handler object. // TODO: This is stupid and racy, get rid of it. TCPNetworkConnection.NetworkHandler networkHandler = (TCPNetworkConnection.NetworkHandler) future.getChannel().getPipeline().get("codec"); if (networkHandler != null) { // This can be null in unit tests or apps that don't use TCP connections. networkHandler.getOwnerObject().setRemoteAddress(address); } if (incrementMaxConnections) { // We don't use setMaxConnections here as that would trigger a recursive attempt to establish a new // outbound connection. lock.lock(); try { maxConnections++; } finally { lock.unlock(); } } return future; } static public Peer peerFromChannelFuture(ChannelFuture future) { return peerFromChannel(future.getChannel()); } static public Peer peerFromChannel(Channel channel) { return ((PeerHandler)channel.getPipeline().get("peer")).getPeer(); } /** * <p>Start downloading the blockchain from the first available peer.</p> * * <p>If no peers are currently connected, the download will be started once a peer starts. If the peer dies, * the download will resume with another peer.</p> * * @param listener a listener for chain download events, may not be null */ public void startBlockChainDownload(PeerEventListener listener) { lock.lock(); try { this.downloadListener = listener; // TODO: be more nuanced about which peer to download from. We can also try // downloading from multiple peers and handle the case when a new peer comes along // with a longer chain after we thought we were done. if (!peers.isEmpty()) { startBlockChainDownloadFromPeer(peers.iterator().next()); } } finally { lock.unlock(); } } /** * Download the blockchain from peers. Convenience that uses a {@link DownloadListener} for you.<p> * * This method waits until the download is complete. "Complete" is defined as downloading * from at least one peer all the blocks that are in that peer's inventory. */ public void downloadBlockChain() { DownloadListener listener = new DownloadListener(); startBlockChainDownload(listener); try { listener.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } protected void handleNewPeer(final Peer peer) { int newSize = -1; lock.lock(); try { // Runs on a netty worker thread for every peer that is newly connected. Peer is not locked at this point. // Sets up the newly connected peer so it can do everything it needs to. log.info("{}: New peer", peer); pendingPeers.remove(peer); peers.add(peer); newSize = peers.size(); // Give the peer a filter that can be used to probabilistically drop transactions that // aren't relevant to our wallet. We may still receive some false positives, which is // OK because it helps improve wallet privacy. Old nodes will just ignore the message. try { if (bloomFilter != null) peer.setBloomFilter(bloomFilter); } catch (IOException e) { // That was quick...already disconnected } // Link the peer to the memory pool so broadcast transactions have their confidence levels updated. peer.setDownloadData(false); // TODO: The peer should calculate the fast catchup time from the added wallets here. for (Wallet wallet : wallets) peer.addWallet(wallet); // Re-evaluate download peers. Peer newDownloadPeer = selectDownloadPeer(peers); if (downloadPeer != newDownloadPeer) { setDownloadPeer(newDownloadPeer); boolean shouldDownloadChain = downloadListener != null && chain != null; if (shouldDownloadChain) { startBlockChainDownloadFromPeer(downloadPeer); } } // Make sure the peer knows how to upload transactions that are requested from us. peer.addEventListener(getDataListener, Threading.SAME_THREAD); // And set up event listeners for clients. This will allow them to find out about new transactions and blocks. for (ListenerRegistration<PeerEventListener> registration : peerEventListeners) { peer.addEventListener(registration.listener, registration.executor); } setupPingingForNewPeer(peer); } finally { lock.unlock(); } final int fNewSize = newSize; for (final ListenerRegistration<PeerEventListener> registration : peerEventListeners) { registration.executor.execute(new Runnable() { @Override public void run() { registration.listener.onPeerConnected(peer, fNewSize); } }); } } private void setupPingingForNewPeer(final Peer peer) { checkState(lock.isHeldByCurrentThread()); if (peer.getPeerVersionMessage().clientVersion < Pong.MIN_PROTOCOL_VERSION) return; if (getPingIntervalMsec() <= 0) return; // Disabled. // Start the process of pinging the peer. Do a ping right now and then ensure there's a fixed delay between // each ping. If the peer is taken out of the peers list then the cycle will stop. // // TODO: This should really be done by a timer integrated with the network thread to avoid races. final Runnable[] pingRunnable = new Runnable[1]; pingRunnable[0] = new Runnable() { private boolean firstRun = true; public void run() { // Ensure that the first ping happens immediately and later pings after the requested delay. if (firstRun) { firstRun = false; try { peer.ping().addListener(this, Threading.SAME_THREAD); } catch (Exception e) { log.warn("{}: Exception whilst trying to ping peer: {}", peer, e.toString()); return; } return; } final long interval = getPingIntervalMsec(); if (interval <= 0) return; // Disabled. final TimerTask task = new TimerTask() { @Override public void run() { try { if (!peers.contains(peer) || !PeerGroup.this.isRunning()) return; // Peer was removed/shut down. peer.ping().addListener(pingRunnable[0], Threading.SAME_THREAD); } catch (Exception e) { log.warn("{}: Exception whilst trying to ping peer: {}", peer, e.toString()); } } }; try { vPingTimer.schedule(task, interval); } catch (IllegalStateException ignored) { // This can happen if there's a shutdown race and this runnable is executing whilst the timer is // simultaneously cancelled. } } }; pingRunnable[0].run(); } private void setDownloadPeer(Peer peer) { lock.lock(); try { if (downloadPeer == peer) { return; } if (chain == null) { // PeerGroup creator did not want us to download any data. We still track the download peer for // informational purposes. downloadPeer = peer; return; } if (downloadPeer != null) { log.info("Unsetting download peer: {}", downloadPeer); downloadPeer.setDownloadData(false); } downloadPeer = peer; if (downloadPeer != null) { log.info("Setting download peer: {}", downloadPeer); downloadPeer.setDownloadData(true); downloadPeer.setDownloadParameters(fastCatchupTimeSecs, bloomFilter != null); } } finally { lock.unlock(); } } /** * Returns the {@link MemoryPool} created by this peer group to synchronize its peers. The pool tracks advertised * and downloaded transactions so their confidence can be measured as a proportion of how many peers announced it. * With an un-tampered with internet connection, the more peers announce a transaction the more confidence you can * have that it's really valid. */ public MemoryPool getMemoryPool() { return memoryPool; } /** * Tells the PeerGroup to download only block headers before a certain time and bodies after that. Call this * before starting block chain download. * Do not use a time > NOW - 1 block, as it will break some block download logic. */ public void setFastCatchupTimeSecs(long secondsSinceEpoch) { lock.lock(); try { Preconditions.checkState(chain == null || !chain.shouldVerifyTransactions(), "Fast catchup is incompatible with fully verifying"); fastCatchupTimeSecs = secondsSinceEpoch; if (downloadPeer != null) { downloadPeer.setDownloadParameters(secondsSinceEpoch, bloomFilter != null); } } finally { lock.unlock(); } } /** * Returns the current fast catchup time. The contents of blocks before this time won't be downloaded as they * cannot contain any interesting transactions. If you use {@link PeerGroup#addWallet(Wallet)} this just returns * the min of the wallets earliest key times. * @return a time in seconds since the epoch */ public long getFastCatchupTimeSecs() { lock.lock(); try { return fastCatchupTimeSecs; } finally { lock.unlock(); } } protected void handlePeerDeath(final Peer peer) { // This can run on any Netty worker thread. Because connectToAnyPeer() must run unlocked to avoid circular // deadlock, this method must run largely unlocked too. Some members are thread-safe and others aren't, so // we synchronize only the parts that need it. // Peer deaths can occur during startup if a connect attempt after peer discovery aborts immediately. final State state = state(); if (state != State.RUNNING && state != State.STARTING) return; int numPeers = 0; int numConnectedPeers = 0; lock.lock(); try { pendingPeers.remove(peer); peers.remove(peer); log.info("{}: Peer died", peer.getAddress()); if (peer == downloadPeer) { log.info("Download peer died. Picking a new one."); setDownloadPeer(null); // Pick a new one and possibly tell it to download the chain. final Peer newDownloadPeer = selectDownloadPeer(peers); if (newDownloadPeer != null) { setDownloadPeer(newDownloadPeer); if (downloadListener != null) { startBlockChainDownloadFromPeer(newDownloadPeer); } } } numPeers = peers.size() + pendingPeers.size(); numConnectedPeers = peers.size(); } finally { lock.unlock(); } // Replace this peer with a new one to keep our connection count up, if necessary. if (numPeers < getMaxConnections()) { try { connectToAnyPeer(); } catch (PeerDiscoveryException e) { log.error(e.getMessage()); } } peer.removeEventListener(getDataListener); for (Wallet wallet : wallets) { peer.removeWallet(wallet); } final int fNumConnectedPeers = numConnectedPeers; for (final ListenerRegistration<PeerEventListener> registration : peerEventListeners) { registration.executor.execute(new Runnable() { @Override public void run() { registration.listener.onPeerDisconnected(peer, fNumConnectedPeers); } }); peer.removeEventListener(registration.listener); } } private void startBlockChainDownloadFromPeer(Peer peer) { lock.lock(); try { peer.addEventListener(downloadListener, Threading.SAME_THREAD); setDownloadPeer(peer); // startBlockChainDownload will setDownloadData(true) on itself automatically. peer.startBlockChainDownload(); } catch (IOException e) { log.error("failed to start block chain download from " + peer, e); } finally { lock.unlock(); } } /** * Returns a future that is triggered when the number of connected peers is equal to the given number of connected * peers. By using this with {@link com.google.devcoin.core.PeerGroup#getMaxConnections()} you can wait until the * network is fully online. To block immediately, just call get() on the result. * * @param numPeers How many peers to wait for. * @return a future that will be triggered when the number of connected peers >= numPeers */ public ListenableFuture<PeerGroup> waitForPeers(final int numPeers) { lock.lock(); try { if (peers.size() >= numPeers) { return Futures.immediateFuture(this); } } finally { lock.unlock(); } final SettableFuture<PeerGroup> future = SettableFuture.create(); addEventListener(new AbstractPeerEventListener() { @Override public void onPeerConnected(Peer peer, int peerCount) { if (peerCount >= numPeers) { future.set(PeerGroup.this); removeEventListener(this); } } }); return future; } /** * Returns the number of connections that are required before transactions will be broadcast. If there aren't * enough, {@link PeerGroup#broadcastTransaction(Transaction)} will wait until the minimum number is reached so * propagation across the network can be observed. If no value has been set using * {@link PeerGroup#setMinBroadcastConnections(int)} a default of half of whatever * {@link com.google.devcoin.core.PeerGroup#getMaxConnections()} returns is used. * @return */ public int getMinBroadcastConnections() { lock.lock(); try { if (minBroadcastConnections == 0) { int max = getMaxConnections(); if (max <= 1) return max; else return (int) Math.round(getMaxConnections() / 2.0); } return minBroadcastConnections; } finally { lock.unlock(); } } /** * See {@link com.google.devcoin.core.PeerGroup#getMinBroadcastConnections()}. */ public void setMinBroadcastConnections(int value) { lock.lock(); try { minBroadcastConnections = value; } finally { lock.unlock(); } } /** * Calls {@link PeerGroup#broadcastTransaction(Transaction,int)} with getMinBroadcastConnections() as the number * of connections to wait for before commencing broadcast. */ public ListenableFuture<Transaction> broadcastTransaction(final Transaction tx) { return broadcastTransaction(tx, Math.max(1, getMinBroadcastConnections())); } /** * <p>Given a transaction, sends it un-announced to one peer and then waits for it to be received back from other * peers. Once all connected peers have announced the transaction, the future will be completed. If anything goes * wrong the exception will be thrown when get() is called, or you can receive it via a callback on the * {@link ListenableFuture}. This method returns immediately, so if you want it to block just call get() on the * result.</p> * * <p>Note that if the PeerGroup is limited to only one connection (discovery is not activated) then the future * will complete as soon as the transaction was successfully written to that peer.</p> * * <p>Other than for sending your own transactions, this method is useful if you have received a transaction from * someone and want to know that it's valid. It's a bit of a weird hack because the current version of the Bitcoin * protocol does not inform you if you send an invalid transaction. Because sending bad transactions counts towards * your DoS limit, be careful with relaying lots of unknown transactions. Otherwise you might get kicked off the * network.</p> * * <p>The transaction won't be sent until there are at least minConnections active connections available. * A good choice for proportion would be between 0.5 and 0.8 but if you want faster transmission during initial * bringup of the peer group you can lower it.</p> */ public ListenableFuture<Transaction> broadcastTransaction(final Transaction tx, final int minConnections) { TransactionBroadcast broadcast = new TransactionBroadcast(this, tx); broadcast.setMinConnections(minConnections); // Send the TX to the wallet once we have a successful broadcast. Futures.addCallback(broadcast.future(), new FutureCallback<Transaction>() { @Override public void onSuccess(Transaction transaction) { // OK, now tell the wallet about the transaction. If the wallet created the transaction then // it already knows and will ignore this. If it's a transaction we received from // somebody else via a side channel and are now broadcasting, this will put it into the // wallet now we know it's valid. for (Wallet wallet : wallets) { // Assumption here is there are no dependencies of the created transaction. // // We may end up with two threads trying to do this in parallel - the wallet will // ignore whichever one loses the race. try { wallet.receivePending(transaction, null); } catch (VerificationException e) { throw new RuntimeException(e); // Cannot fail to verify a tx we created ourselves. } } } @Override public void onFailure(Throwable throwable) { } }); broadcast.broadcast(); return broadcast.future(); } /** * Returns the period between pings for an individual peer. Setting this lower means more accurate and timely ping * times are available via {@link com.google.devcoin.core.Peer#getLastPingTime()} but it increases load on the * remote node. It defaults to 5000. */ public long getPingIntervalMsec() { lock.lock(); try { return pingIntervalMsec; } finally { lock.unlock(); } } /** * Sets the period between pings for an individual peer. Setting this lower means more accurate and timely ping * times are available via {@link com.google.devcoin.core.Peer#getLastPingTime()} but it increases load on the * remote node. It defaults to {@link PeerGroup#DEFAULT_PING_INTERVAL_MSEC}. * Setting the value to be <= 0 disables pinging entirely, although you can still request one yourself * using {@link com.google.devcoin.core.Peer#ping()}. */ public void setPingIntervalMsec(long pingIntervalMsec) { lock.lock(); try { this.pingIntervalMsec = pingIntervalMsec; } finally { lock.unlock(); } } /** * If a peer is connected to that claims to speak a protocol version lower than the given version, it will * be disconnected and another one will be tried instead. */ public void setMinRequiredProtocolVersion(int minRequiredProtocolVersion) { this.vMinRequiredProtocolVersion = minRequiredProtocolVersion; } /** The minimum protocol version required: defaults to the version required for Bloom filtering. */ public int getMinRequiredProtocolVersion() { return vMinRequiredProtocolVersion; } /** * Returns our peers most commonly reported chain height. If multiple heights are tied, the highest is returned. * If no peers are connected, returns zero. */ public int getMostCommonChainHeight() { lock.lock(); try { return getMostCommonChainHeight(this.peers); } finally { lock.unlock(); } } /** * Returns most commonly reported chain height from the given list of {@link Peer}s. * If multiple heights are tied, the highest is returned. If no peers are connected, returns zero. */ public static int getMostCommonChainHeight(final List<Peer> peers) { int s = peers.size(); int[] heights = new int[s]; int[] counts = new int[s]; int maxCount = 0; // Calculate the frequencies of each reported height. for (Peer peer : peers) { int h = (int) peer.getBestHeight(); // Find the index of the peers height in the heights array. for (int cursor = 0; cursor < s; cursor++) { if (heights[cursor] == h) { maxCount = Math.max(++counts[cursor], maxCount); break; } else if (heights[cursor] == 0) { // A new height we didn't see before. checkState(counts[cursor] == 0); heights[cursor] = h; counts[cursor] = 1; maxCount = Math.max(maxCount, 1); break; } } } // Find the heights that have the highest frequencies. int[] freqHeights = new int[s]; int cursor = 0; for (int i = 0; i < s; i++) { if (counts[i] == maxCount) { freqHeights[cursor++] = heights[i]; } } // Return the highest of the most common heights. Arrays.sort(freqHeights); return freqHeights[s - 1]; } private static class PeerAndPing { Peer peer; long pingTime; } /** * Given a list of Peers, return a Peer to be used as the download peer. If you don't want PeerGroup to manage * download peer statuses for you, just override this and always return null. */ protected Peer selectDownloadPeer(List<Peer> peers) { // Characteristics to select for in order of importance: // - Chain height is reasonable (majority of nodes) // - High enough protocol version for the features we want (but we'll settle for less) // - Ping time. if (peers.isEmpty()) return null; // Make sure we don't select a peer that is behind/synchronizing itself. int mostCommonChainHeight = getMostCommonChainHeight(peers); List<Peer> candidates = new ArrayList<Peer>(); for (Peer peer : peers) { if (peer.getBestHeight() == mostCommonChainHeight) candidates.add(peer); } // Of the candidates, find the peers that meet the minimum protocol version we want to target. We could select // the highest version we've seen on the assumption that newer versions are always better but we don't want to // zap peers if they upgrade early. If we can't find any peers that have our preferred protocol version or // better then we'll settle for the highest we found instead. int highestVersion = 0, preferredVersion = 0; final int PREFERRED_VERSION = FilteredBlock.MIN_PROTOCOL_VERSION; for (Peer peer : candidates) { highestVersion = Math.max(peer.getPeerVersionMessage().clientVersion, highestVersion); preferredVersion = Math.min(highestVersion, PREFERRED_VERSION); } List<PeerAndPing> candidates2 = new ArrayList<PeerAndPing>(); for (Peer peer : candidates) { if (peer.getPeerVersionMessage().clientVersion >= preferredVersion) { PeerAndPing pap = new PeerAndPing(); pap.peer = peer; pap.pingTime = peer.getPingTime(); candidates2.add(pap); } } // Sort by ping time. Collections.sort(candidates2, new Comparator<PeerAndPing>() { public int compare(PeerAndPing peerAndPing, PeerAndPing peerAndPing2) { if (peerAndPing.pingTime < peerAndPing2.pingTime) return -1; else if (peerAndPing.pingTime == peerAndPing2.pingTime) return 0; else return 1; } }); return candidates2.get(0).peer; } private static class PeerGroupThreadFactory implements ThreadFactory { static final AtomicInteger poolNumber = new AtomicInteger(1); final ThreadGroup group; final AtomicInteger threadNumber = new AtomicInteger(1); final String namePrefix; PeerGroupThreadFactory() { group = Thread.currentThread().getThreadGroup(); namePrefix = "PeerGroup-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); // Lower the priority of the peer threads. This is to avoid competing with UI threads created by the API // user when doing lots of work, like downloading the block chain. We select a priority level one lower // than the parent thread, or the minimum. t.setPriority(Math.max(Thread.MIN_PRIORITY, Thread.currentThread().getPriority() - 1)); t.setDaemon(true); return t; } } /** * Returns the currently selected download peer. Bear in mind that it may have changed as soon as this method * returns. Can return null if no peer was selected. */ public Peer getDownloadPeer() { lock.lock(); try { return downloadPeer; } finally { lock.unlock(); } } }