From b72c1700865dcc576a83c87dad0f754f2e79e7dd Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 2 Feb 2012 17:34:52 +0100 Subject: [PATCH] API for setting version messages on outbound connections, and implementing BIP 14. Resolves issue 105. --- .../bitcoin/core/NetworkConnection.java | 4 +- src/com/google/bitcoin/core/Peer.java | 24 ++++-- src/com/google/bitcoin/core/PeerGroup.java | 78 ++++++++++++++++--- .../bitcoin/core/TCPNetworkConnection.java | 34 ++++++-- .../google/bitcoin/core/VersionMessage.java | 58 +++++++++++++- .../google/bitcoin/examples/PingService.java | 3 +- .../bitcoin/examples/toywallet/ToyWallet.java | 1 + .../google/bitcoin/core/PeerGroupTest.java | 3 +- 8 files changed, 174 insertions(+), 31 deletions(-) diff --git a/src/com/google/bitcoin/core/NetworkConnection.java b/src/com/google/bitcoin/core/NetworkConnection.java index 7d1196d7..05c15c3c 100644 --- a/src/com/google/bitcoin/core/NetworkConnection.java +++ b/src/com/google/bitcoin/core/NetworkConnection.java @@ -61,7 +61,9 @@ public interface NetworkConnection { */ void writeMessage(Message message) throws IOException; - /** Returns the version message received from the other end of the connection during the handshake. */ + /** + * Returns the version message received from the other end of the connection during the handshake. + */ VersionMessage getVersionMessage(); /** diff --git a/src/com/google/bitcoin/core/Peer.java b/src/com/google/bitcoin/core/Peer.java index 2d622ce0..839a34be 100644 --- a/src/com/google/bitcoin/core/Peer.java +++ b/src/com/google/bitcoin/core/Peer.java @@ -30,6 +30,7 @@ import java.util.concurrent.*; */ public class Peer { private static final Logger log = LoggerFactory.getLogger(Peer.class); + public static final int CONNECT_TIMEOUT_MSEC = 60000; private NetworkConnection conn; private final NetworkParameters params; @@ -49,6 +50,9 @@ public class Peer { // primary peer. This is to avoid redundant work and concurrency problems with downloading the same chain // in parallel. private boolean downloadData = true; + // The version data to announce to the other side of the connections we make: useful for setting our "user agent" + // equivalent and other things. + private VersionMessage versionMessage; /** * Size of the pending transactions pool. Override this to reduce memory usage on constrained platforms. The pool @@ -69,12 +73,6 @@ public class Peer { } }; - /** - * If true, we do some things that may only make sense on constrained devices like Android phones. Currently this - * only controls message deduplication. - */ - public static boolean MOBILE_OPTIMIZED = false; - // A time before which we only download block headers, after that point we download block bodies. private long fastCatchupTimeSecs; // Whether we are currently downloading headers only or block bodies. Defaults to true, if the fast catchup time @@ -89,13 +87,23 @@ public class Peer { * @param bestHeight our current best chain height, to facilitate downloading */ public Peer(NetworkParameters params, PeerAddress address, int bestHeight, BlockChain blockChain) { + this(params, address, blockChain, new VersionMessage(params, bestHeight)); + } + + /** + * Construct a peer that reads/writes from the given block chain. Note that communication won't occur until + * you call connect(), which will set up a new NetworkConnection. + * + * @param ver The version data to announce to the other side. + */ + public Peer(NetworkParameters params, PeerAddress address, BlockChain blockChain, VersionMessage ver) { this.params = params; this.address = address; - this.bestHeight = bestHeight; this.blockChain = blockChain; this.pendingGetBlockFutures = new ArrayList>(); this.eventListeners = new ArrayList(); this.fastCatchupTimeSecs = params.genesisBlock.getTimeSeconds(); + this.versionMessage = ver; } /** @@ -140,7 +148,7 @@ public class Peer { */ public synchronized void connect() throws PeerException { try { - conn = new TCPNetworkConnection(address, params, bestHeight, 60000, MOBILE_OPTIMIZED); + conn = new TCPNetworkConnection(address, params, CONNECT_TIMEOUT_MSEC, false, versionMessage); } catch (IOException ex) { throw new PeerException(ex); } catch (ProtocolException ex) { diff --git a/src/com/google/bitcoin/core/PeerGroup.java b/src/com/google/bitcoin/core/PeerGroup.java index 540c65bf..3309ee75 100644 --- a/src/com/google/bitcoin/core/PeerGroup.java +++ b/src/com/google/bitcoin/core/PeerGroup.java @@ -31,16 +31,16 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; /** - * Maintain a number of connections to peers. - *

- *

PeerGroup tries to maintain a constant number of connections to a set of distinct peers. + * Maintain a number of connections to peers.

+ * + * 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. - *

- *

Connections are made to addresses from a provided list. When that list is exhausted, - * we start again from the head of the list. - *

- *

The PeerGroup can broadcast a transaction to the currently connected set of peers. It can + * will be tried after a delay as long as the number of connections less than the maximum.

+ * + * Connections are made to addresses from a provided list. When that list is exhausted, + * we start again from the head of the list.

+ * + * 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. * * @author miron@google.com (Miron Cuperman a.k.a devrandom) @@ -71,6 +71,8 @@ public class PeerGroup { private List peerEventListeners; // Peer discovery sources, will be polled occasionally if there aren't enough inactives. private Set peerDiscoverers; + // The version message to use for new connections. + private VersionMessage versionMessage; private NetworkParameters params; private BlockChain chain; @@ -101,6 +103,10 @@ public class PeerGroup { this.fastCatchupTimeSecs = params.genesisBlock.getTimeSeconds(); this.wallets = new ArrayList(1); + // Set up a default template version message that doesn't tell the other side what kind of BitCoinJ user + // this is. + this.versionMessage = new VersionMessage(params, chain.getBestChainHeight()); + inactives = new LinkedBlockingQueue(); // TODO: Remove usage of synchronized sets here in favor of simple coarse-grained locking. peers = Collections.synchronizedSet(new HashSet()); @@ -155,6 +161,55 @@ public class PeerGroup { } } + /** + * 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 + * BIP 14. + * + * 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 synchronized void setVersionMessage(VersionMessage ver) { + versionMessage = ver; + } + + /** + * Returns the version message provided by setVersionMessage or a default if none was given. + */ + public synchronized VersionMessage getVersionMessage() { + return versionMessage; + } + + /** + * 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) { + VersionMessage ver = new VersionMessage(params, 0); + ver.appendToSubVer(name, version, comments); + setVersionMessage(ver); + } + + /** + * 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); + } + + /** *

Adds a listener that will be notified on a library controlled thread when:

*
    @@ -407,7 +462,10 @@ public class PeerGroup { final PeerAddress address = inactives.take(); while (true) { try { - Peer peer = new Peer(params, address, chain.getChainHead().getHeight(), chain); + VersionMessage ver = versionMessage.duplicate(); + ver.bestHeight = chain.getBestChainHeight(); + ver.time = Utils.now().getTime() / 1000; + Peer peer = new Peer(params, address, chain, ver); executePeer(address, peer, true, ExecuteBlockMode.RETURN_IMMEDIATELY); break; } catch (RejectedExecutionException e) { diff --git a/src/com/google/bitcoin/core/TCPNetworkConnection.java b/src/com/google/bitcoin/core/TCPNetworkConnection.java index b63c2071..f5676f4b 100644 --- a/src/com/google/bitcoin/core/TCPNetworkConnection.java +++ b/src/com/google/bitcoin/core/TCPNetworkConnection.java @@ -59,14 +59,14 @@ public class TCPNetworkConnection implements NetworkConnection { * @param peerAddress address to connect to. IPv6 is not currently supported by BitCoin. If * port is not positive the default port from params is used. * @param params Defines which network to connect to and details of the protocol. - * @param bestHeight How many blocks are in our best chain * @param connectTimeoutMsec Timeout in milliseconds when initially connecting to peer * @param dedupe Whether to avoid parsing duplicate messages from the network (ie from other peers). + * @param ver The VersionMessage to announce to the other side of the connection. * @throws IOException if there is a network related failure. * @throws ProtocolException if the version negotiation failed. */ public TCPNetworkConnection(PeerAddress peerAddress, NetworkParameters params, - int bestHeight, int connectTimeoutMsec, boolean dedupe) + int connectTimeoutMsec, boolean dedupe, VersionMessage ver) throws IOException, ProtocolException { this.params = params; this.remoteIp = peerAddress.getAddr(); @@ -76,7 +76,7 @@ public class TCPNetworkConnection implements NetworkConnection { InetSocketAddress address = new InetSocketAddress(remoteIp, port); socket = new Socket(); socket.connect(address, connectTimeoutMsec); - + out = socket.getOutputStream(); in = socket.getInputStream(); @@ -87,7 +87,8 @@ public class TCPNetworkConnection implements NetworkConnection { // Announce ourselves. This has to come first to connect to clients beyond v0.30.20.2 which wait to hear // from us until they send their version message back. - writeMessage(new VersionMessage(params, bestHeight)); + log.info("Announcing ourselves as: {}", ver.subVer); + writeMessage(ver); // When connecting, the remote peer sends us a version message with various bits of // useful data in it. We need to know the peer protocol version before we can talk to it. Message m = readMessage(); @@ -104,9 +105,9 @@ public class TCPNetworkConnection implements NetworkConnection { // Switch to the new protocol version. int peerVersion = versionMessage.clientVersion; log.info("Connected to peer: version={}, subVer='{}', services=0x{}, time={}, blocks={}", new Object[] { - peerVersion, - versionMessage.subVer, - versionMessage.localServices, + peerVersion, + versionMessage.subVer, + versionMessage.localServices, new Date(versionMessage.time * 1000), versionMessage.bestHeight }); @@ -126,6 +127,25 @@ public class TCPNetworkConnection implements NetworkConnection { // Handshake is done! } + /** + * Connect to the given IP address using the port specified as part of the network parameters. Once construction + * is complete a functioning network channel is set up and running. + * + * @param peerAddress address to connect to. IPv6 is not currently supported by BitCoin. If + * port is not positive the default port from params is used. + * @param params Defines which network to connect to and details of the protocol. + * @param connectTimeoutMsec Timeout in milliseconds when initially connecting to peer + * @param dedupe Whether to avoid parsing duplicate messages from the network (ie from other peers). + * @param bestHeight The height of the best chain we know about, sent to the other side. + * @throws IOException if there is a network related failure. + * @throws ProtocolException if the version negotiation failed. + */ + public TCPNetworkConnection(PeerAddress peerAddress, NetworkParameters params, + int bestHeight, int connectTimeoutMsec, boolean dedupe) + throws IOException, ProtocolException { + this(peerAddress, params, connectTimeoutMsec, dedupe, new VersionMessage(params, bestHeight)); + } + public TCPNetworkConnection(InetAddress inetAddress, NetworkParameters params, int bestHeight, int connectTimeout) throws IOException, ProtocolException { this(new PeerAddress(inetAddress), params, bestHeight, connectTimeout, true); diff --git a/src/com/google/bitcoin/core/VersionMessage.java b/src/com/google/bitcoin/core/VersionMessage.java index 0c075674..2ea8433b 100644 --- a/src/com/google/bitcoin/core/VersionMessage.java +++ b/src/com/google/bitcoin/core/VersionMessage.java @@ -21,6 +21,15 @@ import java.io.OutputStream; import java.net.InetAddress; import java.net.UnknownHostException; +/** + * A VersionMessage holds information exchanged during connection setup with another peer. Most of the fields are not + * particularly interesting. The subVer field, since BIP 14, acts as a User-Agent string would. You can and should + * append to or change the subVer for your own software so other implementations can identify it, and you can look at + * the subVer field received from other nodes to see what they are running. If blank, it means the Satoshi client.

    + * + * After creating yourself a VersionMessage, you can pass it to {@link PeerGroup#setVersionMessage(VersionMessage)} + * to ensure it will be used for each new connection. + */ public class VersionMessage extends Message { private static final long serialVersionUID = 7313594258967483180L; @@ -94,7 +103,7 @@ public class VersionMessage extends Message { @Override protected void parseLite() throws ProtocolException { - //NOP. VersionMessage is never lazy parsed. + // NOP. VersionMessage is never lazy parsed. } @Override @@ -185,7 +194,6 @@ public class VersionMessage extends Message { */ @Override void setChecksum(byte[] checksum) { - } @Override @@ -207,4 +215,50 @@ public class VersionMessage extends Message { return sb.toString(); } + public VersionMessage duplicate() { + VersionMessage v = new VersionMessage(params, (int) bestHeight); + v.clientVersion = clientVersion; + v.localServices = localServices; + v.time = time; + v.myAddr = myAddr; + v.theirAddr = theirAddr; + v.subVer = subVer; + return v; + } + + /** + * Appends the given user-agent information to the subVer field. The subVer is composed of a series of + * name:version pairs separated by slashes in the form of a path. For example a typical subVer field for BitCoinJ + * users might look like "/BitCoinJ:0.4-SNAPSHOT/MultiBit:1.2/" where libraries come further to the left.

    + * + * There can be as many components as you feel a need for, and the version string can be anything, but it is + * recommended to use A.B.C where A = major, B = minor and C = revision for software releases, and dates for + * auto-generated source repository snapshots. A valid subVer begins and ends with a slash, therefore name + * and version are not allowed to contain such characters.

    + * + * Anything put in the "comments" field will appear in brackets and may be used for platform info, or anything + * else. For example, calling appendToSubVer("MultiBit", "1.0", "Windows") will result in a subVer being + * set of "/BitCoinJ:1.0/MultiBit:1.0(Windows)/. Therefore the / ( and ) characters are reserved in all these + * components. If you don't want to add a comment (recommended), pass null.

    + * + * See BIP 14 for more information. + * + * @param comments Optional (can be null) platform or other node specific information. + * @throws IllegalArgumentException if name, version or comments contains invalid characters. + */ + public void appendToSubVer(String name, String version, String comments) { + checkSubVerComponent(name); + checkSubVerComponent(version); + if (comments != null) { + checkSubVerComponent(comments); + subVer = subVer.concat(String.format("%s:%s(%s)/", name, version, comments)); + } else { + subVer = subVer.concat(String.format("%s:%s/", name, version)); + } + } + + private void checkSubVerComponent(String component) { + if (component.contains("/") || component.contains("(") || component.contains(")")) + throw new IllegalArgumentException("name contains invalid characters"); + } } diff --git a/src/com/google/bitcoin/examples/PingService.java b/src/com/google/bitcoin/examples/PingService.java index 13939779..2d4cbded 100644 --- a/src/com/google/bitcoin/examples/PingService.java +++ b/src/com/google/bitcoin/examples/PingService.java @@ -98,7 +98,8 @@ public class PingService { chain = new BlockChain(params, wallet, blockStore); peerGroup = new PeerGroup(params, chain); - + // Set some version info. + peerGroup.setUserAgent("PingService", "1.0"); // Download headers only until a day ago. peerGroup.setFastCatchupTimeSecs((new Date().getTime() / 1000) - (60 * 60 * 24)); if (peerHost != null) { diff --git a/src/com/google/bitcoin/examples/toywallet/ToyWallet.java b/src/com/google/bitcoin/examples/toywallet/ToyWallet.java index 9f456a08..36f4e3fc 100644 --- a/src/com/google/bitcoin/examples/toywallet/ToyWallet.java +++ b/src/com/google/bitcoin/examples/toywallet/ToyWallet.java @@ -105,6 +105,7 @@ public class ToyWallet { chain = new BlockChain(params, wallet, new BoundedOverheadBlockStore(params, blockChainFile)); peerGroup = new PeerGroup(params, chain); + peerGroup.setUserAgent("ToyWallet", "1.0"); if (testnet) { peerGroup.addAddress(new PeerAddress(InetAddress.getByName("plan99.net"), 18333)); peerGroup.addAddress(new PeerAddress(InetAddress.getByName("localhost"), 18333)); diff --git a/tests/com/google/bitcoin/core/PeerGroupTest.java b/tests/com/google/bitcoin/core/PeerGroupTest.java index 2990a1b5..5dc25082 100644 --- a/tests/com/google/bitcoin/core/PeerGroupTest.java +++ b/tests/com/google/bitcoin/core/PeerGroupTest.java @@ -292,8 +292,7 @@ public class PeerGroupTest extends TestWithNetworkConnections { assertTrue(n3.outbound() instanceof InventoryMessage); peerGroup.stop(); } - - + private void disconnectAndWait(MockNetworkConnection conn) throws IOException, InterruptedException { conn.disconnect(); disconnectedPeers.take();