diff --git a/src/com/google/bitcoin/core/AddressMessage.java b/src/com/google/bitcoin/core/AddressMessage.java index 65890291..7fe4c192 100644 --- a/src/com/google/bitcoin/core/AddressMessage.java +++ b/src/com/google/bitcoin/core/AddressMessage.java @@ -24,7 +24,7 @@ public class AddressMessage extends Message { throw new ProtocolException("Address message too large."); addresses = new ArrayList((int)numAddresses); for (int i = 0; i < numAddresses; i++) { - PeerAddress addr = new PeerAddress(params, bytes, cursor); + PeerAddress addr = new PeerAddress(params, bytes, cursor, protocolVersion); addresses.add(addr); cursor += addr.getMessageSize(); } diff --git a/src/com/google/bitcoin/core/GetBlocksMessage.java b/src/com/google/bitcoin/core/GetBlocksMessage.java index 24a7d211..0866cc7c 100644 --- a/src/com/google/bitcoin/core/GetBlocksMessage.java +++ b/src/com/google/bitcoin/core/GetBlocksMessage.java @@ -48,7 +48,7 @@ public class GetBlocksMessage extends Message { try { ByteArrayOutputStream buf = new ByteArrayOutputStream(); // Version, for some reason. - Utils.uint32ToByteStreamLE(VersionMessage.PROTOCOL_VERSION, buf); + Utils.uint32ToByteStreamLE(NetworkParameters.PROTOCOL_VERSION, buf); // Then a vector of block hashes. This is actually a "block locator", a set of block // identifiers that spans the entire chain with exponentially increasing gaps between // them, until we end up at the genesis block. See CBlockLocator::Set() diff --git a/src/com/google/bitcoin/core/Message.java b/src/com/google/bitcoin/core/Message.java index 4a26cf8b..41401f99 100644 --- a/src/com/google/bitcoin/core/Message.java +++ b/src/com/google/bitcoin/core/Message.java @@ -16,10 +16,7 @@ package com.google.bitcoin.core; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Serializable; +import java.io.*; import java.math.BigInteger; import java.util.Arrays; @@ -44,8 +41,11 @@ public abstract class Message implements Serializable { // Note that it's relative to the start of the array NOT the start of the message. protected transient int cursor; + // The raw message bytes themselves. protected transient byte[] bytes; + protected transient int protocolVersion; + // This will be saved by subclasses that implement Serializable. protected NetworkParameters params; @@ -58,7 +58,8 @@ public abstract class Message implements Serializable { } @SuppressWarnings("unused") - Message(NetworkParameters params, byte[] msg, int offset) throws ProtocolException { + Message(NetworkParameters params, byte[] msg, int offset, int protocolVersion) throws ProtocolException { + this.protocolVersion = protocolVersion; this.params = params; this.bytes = msg; this.cursor = this.offset = offset; @@ -74,6 +75,10 @@ public abstract class Message implements Serializable { } this.bytes = null; } + + Message(NetworkParameters params, byte[] msg, int offset) throws ProtocolException { + this(params, msg, offset, NetworkParameters.PROTOCOL_VERSION); + } // These methods handle the serialization/deserialization using the custom BitCoin protocol. // It's somewhat painful to work with in Java, so some of these objects support a second @@ -141,4 +146,20 @@ public abstract class Message implements Serializable { cursor += length; return b; } + + String readStr() { + VarInt varInt = new VarInt(bytes, cursor); + if (varInt.value == 0) { + cursor += 1; + return ""; + } + byte[] characters = new byte[(int)varInt.value]; + System.arraycopy(bytes, cursor, characters, 0, characters.length); + cursor += varInt.getSizeInBytes(); + try { + return new String(characters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); // Cannot happen, UTF-8 is always supported. + } + } } diff --git a/src/com/google/bitcoin/core/NetworkConnection.java b/src/com/google/bitcoin/core/NetworkConnection.java index 97d2c16e..d2b480fb 100644 --- a/src/com/google/bitcoin/core/NetworkConnection.java +++ b/src/com/google/bitcoin/core/NetworkConnection.java @@ -55,7 +55,8 @@ public class NetworkConnection { private final InetAddress remoteIp; private boolean usesChecksumming; private final NetworkParameters params; - static final private boolean PROTOCOL_LOG = false; + private final VersionMessage versionMessage; + private static final boolean PROTOCOL_LOG = false; /** * Connect to the given IP address using the port specified as part of the network parameters. Once construction @@ -74,7 +75,7 @@ public class NetworkConnection { // 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. - VersionMessage ver = (VersionMessage) readMessage(); + versionMessage = (VersionMessage) readMessage(); // Now it's our turn ... writeMessage(MSG_VERSION, new VersionMessage(params)); // Send an ACK message stating we accept the peers protocol version. @@ -82,12 +83,13 @@ public class NetworkConnection { // And get one back ... readMessage(); // Switch to the new protocol version. - int peerVersion = ver.clientVersion; - LOG("Connected to peer, version is " + peerVersion + ", services=" + Long.toHexString( - ver.localServices) + ", time=" + new Date(ver.time.longValue() * 1000).toString()); + int peerVersion = versionMessage.clientVersion; + LOG(String.format("Connected to peer: version=%d, subVer='%s', services=0x%x, time=%s, blocks=%d", + peerVersion, versionMessage.subVer, + versionMessage.localServices, new Date(versionMessage.time * 1000).toString(), versionMessage.bestHeight)); // BitCoinJ is a client mode implementation. That means there's not much point in us talking to other client // mode nodes because we can't download the data from them we need to find/verify transactions. - if (!ver.hasBlockChain()) + if (!versionMessage.hasBlockChain()) throw new ProtocolException("Peer does not have a copy of the block chain."); usesChecksumming = peerVersion >= 209; // Handshake is done! @@ -198,9 +200,6 @@ public class NetworkConnection { if (size > Message.MAX_SIZE) throw new ProtocolException("Message size too large: " + size); - if (PROTOCOL_LOG) - LOG("Received " + size + " byte '" + command + "' command"); - // Old clients don't send the checksum. byte[] checksum = new byte[4]; if (usesChecksumming) { @@ -231,6 +230,9 @@ public class NetworkConnection { } } + if (PROTOCOL_LOG) + LOG("Received " + size + " byte '" + command + "' message: " + Utils.bytesToHexString(payloadBytes)); + try { Message message; if (command.equals(MSG_VERSION)) @@ -292,4 +294,9 @@ public class NetworkConnection { // TODO: Requiring "tag" here is redundant, the message object should know its own protocol tag. writeMessage(tag, message.bitcoinSerialize()); } + + /** Returns the version message received from the other end of the connection during the handshake. */ + public VersionMessage getVersionMessage() { + return versionMessage; + } } diff --git a/src/com/google/bitcoin/core/NetworkParameters.java b/src/com/google/bitcoin/core/NetworkParameters.java index 3103fda1..04306005 100644 --- a/src/com/google/bitcoin/core/NetworkParameters.java +++ b/src/com/google/bitcoin/core/NetworkParameters.java @@ -29,6 +29,11 @@ import java.math.BigInteger; * evolves there may be more. You can create your own as long as they don't conflict. */ public class NetworkParameters implements Serializable { + /** + * The protocol version this library implements. A value of 31800 means 0.3.18.00. + */ + public static final int PROTOCOL_VERSION = 31800; + private static final long serialVersionUID = 2579833727976661964L; // TODO: Seed nodes and checkpoint values should be here as well. diff --git a/src/com/google/bitcoin/core/Peer.java b/src/com/google/bitcoin/core/Peer.java index 41fb2594..d2c8b663 100644 --- a/src/com/google/bitcoin/core/Peer.java +++ b/src/com/google/bitcoin/core/Peer.java @@ -106,9 +106,6 @@ public class Peer { } } - // This tracks whether we have received a block we could not connect to the chain in this session. - private boolean hasSeenUnconnectedBlock = false; - private void processBlock(Block m) throws IOException { assert Thread.currentThread() == thread; try { @@ -128,17 +125,13 @@ public class Peer { // Otherwise it's a block sent to us because the peer thought we needed it, so add it to the block chain. // This call will synchronize on blockChain. if (blockChain.add(m)) { - // The block was successfully linked into the chain. - if (hasSeenUnconnectedBlock && blockChain.getUnconnectedBlock() == null) { - // We cleared out our unconnected blocks. This likely means block chain download is "done" in the - // sense that we were downloading blocks as part of the chained download, - // and there's no more to come. To some extent of course the download is never done. - LOG("Block chain download done."); - if (chainCompletionLatch != null) { - chainCompletionLatch.countDown(); + // The block was successfully linked into the chain. Notify the user of our progress. + if (chainCompletionLatch != null) { + chainCompletionLatch.countDown(); + if (chainCompletionLatch.getCount() == 0) { + // All blocks fetched, so we don't need this anymore. chainCompletionLatch = null; } - hasSeenUnconnectedBlock = false; } } else { // This block is unconnected - we don't know how to get from it back to the genesis block yet. That @@ -148,7 +141,6 @@ public class Peer { // the others. // TODO: Should actually request root of orphan chain here. - hasSeenUnconnectedBlock = true; blockChainDownload(m.getHash()); } } catch (VerificationException e) { @@ -319,14 +311,20 @@ public class Peer { } /** - * Starts an asynchronous download of the block chain. Completion of the download is a somewhat vague concept in - * BitCoin as the chain is constantly growing, but essentially we deem the download complete once we have - * received the block that the peer told us was the head when we first started the download. + * Starts an asynchronous download of the block chain. The chain download is deemed to be complete once we've + * downloaded the same number of blocks that the peer advertised having in its version handshake message. * - * @return a {@link CountDownLatch} that can be used to wait until the chain download is "complete". + * @return a {@link CountDownLatch} that can be used to track progress and wait for completion. */ public CountDownLatch startBlockChainDownload() throws IOException { - chainCompletionLatch = new CountDownLatch(1); + // Chain will overflow signed int blocks in ~41,000 years. + int chainHeight = (int) conn.getVersionMessage().bestHeight; + if (chainHeight <= 0) { + // This should not happen because we shouldn't have given the user a Peer that is to another client-mode + // node. If that happens it means the user overrode us somewhere. + throw new RuntimeException("Peer does not have block chain"); + } + chainCompletionLatch = new CountDownLatch(chainHeight); blockChainDownload(params.genesisBlock.getHash()); return chainCompletionLatch; } diff --git a/src/com/google/bitcoin/core/PeerAddress.java b/src/com/google/bitcoin/core/PeerAddress.java index b554caa0..c3b417f7 100644 --- a/src/com/google/bitcoin/core/PeerAddress.java +++ b/src/com/google/bitcoin/core/PeerAddress.java @@ -36,9 +36,10 @@ public class PeerAddress extends Message { InetAddress addr; int port; BigInteger services; + long time; - public PeerAddress(NetworkParameters params, byte[] payload, int offset) throws ProtocolException { - super(params, payload, offset); + public PeerAddress(NetworkParameters params, byte[] payload, int offset, int protocolVersion) throws ProtocolException { + super(params, payload, offset, protocolVersion); } public PeerAddress(InetAddress addr, int port) { @@ -72,7 +73,10 @@ public class PeerAddress extends Message { // uint64 services (flags determining what the node can do) // 16 bytes ip address // 2 bytes port num - long time = readUint32(); + if (protocolVersion > 31402) + time = readUint32(); + else + time = -1; services = readUint64(); byte[] addrBytes = readBytes(16); try { diff --git a/src/com/google/bitcoin/core/VersionMessage.java b/src/com/google/bitcoin/core/VersionMessage.java index 63098584..d273cfce 100644 --- a/src/com/google/bitcoin/core/VersionMessage.java +++ b/src/com/google/bitcoin/core/VersionMessage.java @@ -25,22 +25,28 @@ import java.net.UnknownHostException; public class VersionMessage extends Message { private static final long serialVersionUID = 7313594258967483180L; - /** - * The protocol version this library implements. A value of 31800 means 0.3.18.00. - */ - public static final int PROTOCOL_VERSION = 31800; - /** * A services flag that denotes whether the peer has a copy of the block chain or not. */ public static final int NODE_NETWORK = 1; + /** The version number of the protocol spoken. */ public int clientVersion; - // Flags defining what the other side supports. Right now there's only one flag and it's - // always set 1 by the official client, but we have to set it to zero as we don't store - // the block chain. In future there may be more services bits. + /** Flags defining what is supported. Right now {@link #NODE_NETWORK} is the only flag defined. */ public long localServices; - public BigInteger time; + /** What the other side believes the current time to be, in seconds. */ + public long time; + /** What the other side believes the address of this program is. Not used. */ + public PeerAddress myAddr; + /** What the other side believes their own address is. Not used. */ + public PeerAddress theirAddr; + /** + * An additional string that today the official client sets to the empty string. We treat it as something like an + * HTTP User-Agent header. + */ + public String subVer; + /** How many blocks are in the chain, according to the other side. */ + public long bestHeight; public VersionMessage(NetworkParameters params, byte[] msg) throws ProtocolException { super(params, msg, 0); @@ -48,43 +54,51 @@ public class VersionMessage extends Message { public VersionMessage(NetworkParameters params) { super(params); - clientVersion = PROTOCOL_VERSION; + clientVersion = NetworkParameters.PROTOCOL_VERSION; localServices = 0; - time = BigInteger.valueOf(System.currentTimeMillis() / 1000); + time = System.currentTimeMillis() / 1000; + // Note that the official client doesn't do anything with these, and finding out your own external IP address + // is kind of tricky anyway, so we just put nonsense here for now. + try { + myAddr = new PeerAddress(InetAddress.getLocalHost(), params.port); + theirAddr = new PeerAddress(InetAddress.getLocalHost(), params.port); + } catch (UnknownHostException e) { + throw new RuntimeException(e); // Cannot happen. + } + subVer = "BitCoinJ 0.1.99"; + bestHeight = 0; } @Override public void parse() throws ProtocolException { - // There is probably a more Java-ish way to do this. clientVersion = (int) readUint32(); localServices = readUint64().longValue(); - time = readUint64(); - // The next fields are: - // CAddress my address - // CAddress their address - // uint64 localHostNonce (random data) + time = readUint64().longValue(); + myAddr = new PeerAddress(params, bytes, cursor, 0); + cursor += myAddr.getMessageSize(); + theirAddr = new PeerAddress(params, bytes, cursor, 0); + cursor += theirAddr.getMessageSize(); + // uint64 localHostNonce (random data) + // We don't care about the localhost nonce. It's used to detect connecting back to yourself in cases where + // there are NATs and proxies in the way. However we don't listen for inbound connections so it's irrelevant. + readUint64(); // string subVer (currently "") + subVer = readStr(); // int bestHeight (size of known block chain). - // - // However, we don't care about these fields right now. + bestHeight = readUint32(); } - @Override public void bitcoinSerializeToStream(OutputStream buf) throws IOException { Utils.uint32ToByteStreamLE(clientVersion, buf); Utils.uint32ToByteStreamLE(localServices, buf); - long ltime = time.longValue(); - Utils.uint32ToByteStreamLE(ltime >> 32, buf); - Utils.uint32ToByteStreamLE(ltime, buf); + Utils.uint32ToByteStreamLE(time >> 32, buf); + Utils.uint32ToByteStreamLE(time, buf); try { - // Now there are two address structures. Note that the official client doesn't do anything with these, and - // finding out your own external IP address is kind of tricky anyway, so we just serialize nonsense here. - // My address. - new PeerAddress(InetAddress.getLocalHost(), params.port).bitcoinSerializeToStream(buf); + myAddr.bitcoinSerializeToStream(buf); // Their address. - new PeerAddress(InetAddress.getLocalHost(), params.port).bitcoinSerializeToStream(buf); + theirAddr.bitcoinSerializeToStream(buf); } catch (UnknownHostException e) { throw new RuntimeException(e); // Can't happen. } catch (IOException e) { @@ -95,10 +109,12 @@ public class VersionMessage extends Message { // connections. Utils.uint32ToByteStreamLE(0, buf); Utils.uint32ToByteStreamLE(0, buf); - // Now comes an empty string. - buf.write(0); - // Size of known block chain. Claim we never saw any blocks. - Utils.uint32ToByteStreamLE(0, buf); + // Now comes subVer. + byte[] subVerBytes = subVer.getBytes("UTF-8"); + buf.write(new VarInt(subVerBytes.length).encode()); + buf.write(subVerBytes); + // Size of known block chain. + Utils.uint32ToByteStreamLE(bestHeight, buf); } /** diff --git a/src/com/google/bitcoin/examples/PingService.java b/src/com/google/bitcoin/examples/PingService.java index 3b47d7af..0e642f03 100644 --- a/src/com/google/bitcoin/examples/PingService.java +++ b/src/com/google/bitcoin/examples/PingService.java @@ -22,6 +22,8 @@ import java.io.File; import java.io.IOException; import java.math.BigInteger; import java.net.InetAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * PingService demonstrates basic usage of the library. It sits on the network and when it receives coins, simply @@ -29,7 +31,7 @@ import java.net.InetAddress; */ public class PingService { public static void main(String[] args) throws Exception { - final NetworkParameters params = NetworkParameters.prodNet(); + final NetworkParameters params = NetworkParameters.testNet(); // Try to read the wallet from storage, create a new one if not possible. Wallet wallet; @@ -51,7 +53,6 @@ public class PingService { BlockChain chain = new BlockChain(params, wallet); final Peer peer = new Peer(params, conn, chain); peer.start(); - peer.startBlockChainDownload().await(); // We want to know when the balance changes. wallet.addEventListener(new WalletEventListener() { @@ -82,6 +83,15 @@ public class PingService { } }); + CountDownLatch progress = peer.startBlockChainDownload(); + long max = progress.getCount(); // Racy but no big deal. + long current = max; + while (current > 0) { + double pct = 100.0 - (100.0 * (current / (double)max)); + System.out.println(String.format("Chain download %d%% done", (int)pct)); + progress.await(1, TimeUnit.SECONDS); + current = progress.getCount(); + } System.out.println("Send coins to: " + key.toAddress(params).toString()); System.out.println("Waiting for coins to arrive. Press Ctrl-C to quit."); // The peer thread keeps us alive until something kills the process.