Some changes to how block chain download is done:

- Progress is now made available
- Fixes bug: can now wait for downloads of chains < 500 blocks
- Flesh out VersionMessage parsing, send BitCoinJ name in subVer field
This commit is contained in:
Mike Hearn
2011-03-15 18:06:15 +00:00
parent c40b7ce668
commit 57caa5503d
9 changed files with 132 additions and 71 deletions

View File

@@ -24,7 +24,7 @@ public class AddressMessage extends Message {
throw new ProtocolException("Address message too large."); throw new ProtocolException("Address message too large.");
addresses = new ArrayList<PeerAddress>((int)numAddresses); addresses = new ArrayList<PeerAddress>((int)numAddresses);
for (int i = 0; i < numAddresses; i++) { 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); addresses.add(addr);
cursor += addr.getMessageSize(); cursor += addr.getMessageSize();
} }

View File

@@ -48,7 +48,7 @@ public class GetBlocksMessage extends Message {
try { try {
ByteArrayOutputStream buf = new ByteArrayOutputStream(); ByteArrayOutputStream buf = new ByteArrayOutputStream();
// Version, for some reason. // 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 // 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 // identifiers that spans the entire chain with exponentially increasing gaps between
// them, until we end up at the genesis block. See CBlockLocator::Set() // them, until we end up at the genesis block. See CBlockLocator::Set()

View File

@@ -16,10 +16,7 @@
package com.google.bitcoin.core; package com.google.bitcoin.core;
import java.io.ByteArrayOutputStream; import java.io.*;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Arrays; 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. // Note that it's relative to the start of the array NOT the start of the message.
protected transient int cursor; protected transient int cursor;
// The raw message bytes themselves.
protected transient byte[] bytes; protected transient byte[] bytes;
protected transient int protocolVersion;
// This will be saved by subclasses that implement Serializable. // This will be saved by subclasses that implement Serializable.
protected NetworkParameters params; protected NetworkParameters params;
@@ -58,7 +58,8 @@ public abstract class Message implements Serializable {
} }
@SuppressWarnings("unused") @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.params = params;
this.bytes = msg; this.bytes = msg;
this.cursor = this.offset = offset; this.cursor = this.offset = offset;
@@ -75,6 +76,10 @@ public abstract class Message implements Serializable {
this.bytes = null; 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. // 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 // It's somewhat painful to work with in Java, so some of these objects support a second
// serialization mechanism - the standard Java serialization system. This is used when things // serialization mechanism - the standard Java serialization system. This is used when things
@@ -141,4 +146,20 @@ public abstract class Message implements Serializable {
cursor += length; cursor += length;
return b; 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.
}
}
} }

View File

@@ -55,7 +55,8 @@ public class NetworkConnection {
private final InetAddress remoteIp; private final InetAddress remoteIp;
private boolean usesChecksumming; private boolean usesChecksumming;
private final NetworkParameters params; 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 * 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 // 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. // 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 ... // Now it's our turn ...
writeMessage(MSG_VERSION, new VersionMessage(params)); writeMessage(MSG_VERSION, new VersionMessage(params));
// Send an ACK message stating we accept the peers protocol version. // Send an ACK message stating we accept the peers protocol version.
@@ -82,12 +83,13 @@ public class NetworkConnection {
// And get one back ... // And get one back ...
readMessage(); readMessage();
// Switch to the new protocol version. // Switch to the new protocol version.
int peerVersion = ver.clientVersion; int peerVersion = versionMessage.clientVersion;
LOG("Connected to peer, version is " + peerVersion + ", services=" + Long.toHexString( LOG(String.format("Connected to peer: version=%d, subVer='%s', services=0x%x, time=%s, blocks=%d",
ver.localServices) + ", time=" + new Date(ver.time.longValue() * 1000).toString()); 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 // 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. // 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."); throw new ProtocolException("Peer does not have a copy of the block chain.");
usesChecksumming = peerVersion >= 209; usesChecksumming = peerVersion >= 209;
// Handshake is done! // Handshake is done!
@@ -198,9 +200,6 @@ public class NetworkConnection {
if (size > Message.MAX_SIZE) if (size > Message.MAX_SIZE)
throw new ProtocolException("Message size too large: " + 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. // Old clients don't send the checksum.
byte[] checksum = new byte[4]; byte[] checksum = new byte[4];
if (usesChecksumming) { if (usesChecksumming) {
@@ -231,6 +230,9 @@ public class NetworkConnection {
} }
} }
if (PROTOCOL_LOG)
LOG("Received " + size + " byte '" + command + "' message: " + Utils.bytesToHexString(payloadBytes));
try { try {
Message message; Message message;
if (command.equals(MSG_VERSION)) 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. // TODO: Requiring "tag" here is redundant, the message object should know its own protocol tag.
writeMessage(tag, message.bitcoinSerialize()); writeMessage(tag, message.bitcoinSerialize());
} }
/** Returns the version message received from the other end of the connection during the handshake. */
public VersionMessage getVersionMessage() {
return versionMessage;
}
} }

View File

@@ -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. * evolves there may be more. You can create your own as long as they don't conflict.
*/ */
public class NetworkParameters implements Serializable { 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; private static final long serialVersionUID = 2579833727976661964L;
// TODO: Seed nodes and checkpoint values should be here as well. // TODO: Seed nodes and checkpoint values should be here as well.

View File

@@ -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 { private void processBlock(Block m) throws IOException {
assert Thread.currentThread() == thread; assert Thread.currentThread() == thread;
try { 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. // 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. // This call will synchronize on blockChain.
if (blockChain.add(m)) { if (blockChain.add(m)) {
// The block was successfully linked into the chain. // The block was successfully linked into the chain. Notify the user of our progress.
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) { if (chainCompletionLatch != null) {
chainCompletionLatch.countDown(); chainCompletionLatch.countDown();
if (chainCompletionLatch.getCount() == 0) {
// All blocks fetched, so we don't need this anymore.
chainCompletionLatch = null; chainCompletionLatch = null;
} }
hasSeenUnconnectedBlock = false;
} }
} else { } else {
// This block is unconnected - we don't know how to get from it back to the genesis block yet. That // 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. // the others.
// TODO: Should actually request root of orphan chain here. // TODO: Should actually request root of orphan chain here.
hasSeenUnconnectedBlock = true;
blockChainDownload(m.getHash()); blockChainDownload(m.getHash());
} }
} catch (VerificationException e) { } 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 * Starts an asynchronous download of the block chain. The chain download is deemed to be complete once we've
* BitCoin as the chain is constantly growing, but essentially we deem the download complete once we have * downloaded the same number of blocks that the peer advertised having in its version handshake message.
* received the block that the peer told us was the head when we first started the download.
* *
* @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 { 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()); blockChainDownload(params.genesisBlock.getHash());
return chainCompletionLatch; return chainCompletionLatch;
} }

View File

@@ -36,9 +36,10 @@ public class PeerAddress extends Message {
InetAddress addr; InetAddress addr;
int port; int port;
BigInteger services; BigInteger services;
long time;
public PeerAddress(NetworkParameters params, byte[] payload, int offset) throws ProtocolException { public PeerAddress(NetworkParameters params, byte[] payload, int offset, int protocolVersion) throws ProtocolException {
super(params, payload, offset); super(params, payload, offset, protocolVersion);
} }
public PeerAddress(InetAddress addr, int port) { public PeerAddress(InetAddress addr, int port) {
@@ -72,7 +73,10 @@ public class PeerAddress extends Message {
// uint64 services (flags determining what the node can do) // uint64 services (flags determining what the node can do)
// 16 bytes ip address // 16 bytes ip address
// 2 bytes port num // 2 bytes port num
long time = readUint32(); if (protocolVersion > 31402)
time = readUint32();
else
time = -1;
services = readUint64(); services = readUint64();
byte[] addrBytes = readBytes(16); byte[] addrBytes = readBytes(16);
try { try {

View File

@@ -25,22 +25,28 @@ import java.net.UnknownHostException;
public class VersionMessage extends Message { public class VersionMessage extends Message {
private static final long serialVersionUID = 7313594258967483180L; 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. * A services flag that denotes whether the peer has a copy of the block chain or not.
*/ */
public static final int NODE_NETWORK = 1; public static final int NODE_NETWORK = 1;
/** The version number of the protocol spoken. */
public int clientVersion; public int clientVersion;
// Flags defining what the other side supports. Right now there's only one flag and it's /** Flags defining what is supported. Right now {@link #NODE_NETWORK} is the only flag defined. */
// 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.
public long localServices; 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 { public VersionMessage(NetworkParameters params, byte[] msg) throws ProtocolException {
super(params, msg, 0); super(params, msg, 0);
@@ -48,43 +54,51 @@ public class VersionMessage extends Message {
public VersionMessage(NetworkParameters params) { public VersionMessage(NetworkParameters params) {
super(params); super(params);
clientVersion = PROTOCOL_VERSION; clientVersion = NetworkParameters.PROTOCOL_VERSION;
localServices = 0; 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 @Override
public void parse() throws ProtocolException { public void parse() throws ProtocolException {
// There is probably a more Java-ish way to do this.
clientVersion = (int) readUint32(); clientVersion = (int) readUint32();
localServices = readUint64().longValue(); localServices = readUint64().longValue();
time = readUint64(); time = readUint64().longValue();
// The next fields are: myAddr = new PeerAddress(params, bytes, cursor, 0);
// CAddress my address cursor += myAddr.getMessageSize();
// CAddress their address theirAddr = new PeerAddress(params, bytes, cursor, 0);
cursor += theirAddr.getMessageSize();
// uint64 localHostNonce (random data) // 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 "") // string subVer (currently "")
subVer = readStr();
// int bestHeight (size of known block chain). // int bestHeight (size of known block chain).
// bestHeight = readUint32();
// However, we don't care about these fields right now.
} }
@Override @Override
public void bitcoinSerializeToStream(OutputStream buf) throws IOException { public void bitcoinSerializeToStream(OutputStream buf) throws IOException {
Utils.uint32ToByteStreamLE(clientVersion, buf); Utils.uint32ToByteStreamLE(clientVersion, buf);
Utils.uint32ToByteStreamLE(localServices, buf); Utils.uint32ToByteStreamLE(localServices, buf);
long ltime = time.longValue(); Utils.uint32ToByteStreamLE(time >> 32, buf);
Utils.uint32ToByteStreamLE(ltime >> 32, buf); Utils.uint32ToByteStreamLE(time, buf);
Utils.uint32ToByteStreamLE(ltime, buf);
try { 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. // My address.
new PeerAddress(InetAddress.getLocalHost(), params.port).bitcoinSerializeToStream(buf); myAddr.bitcoinSerializeToStream(buf);
// Their address. // Their address.
new PeerAddress(InetAddress.getLocalHost(), params.port).bitcoinSerializeToStream(buf); theirAddr.bitcoinSerializeToStream(buf);
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
throw new RuntimeException(e); // Can't happen. throw new RuntimeException(e); // Can't happen.
} catch (IOException e) { } catch (IOException e) {
@@ -95,10 +109,12 @@ public class VersionMessage extends Message {
// connections. // connections.
Utils.uint32ToByteStreamLE(0, buf); Utils.uint32ToByteStreamLE(0, buf);
Utils.uint32ToByteStreamLE(0, buf); Utils.uint32ToByteStreamLE(0, buf);
// Now comes an empty string. // Now comes subVer.
buf.write(0); byte[] subVerBytes = subVer.getBytes("UTF-8");
// Size of known block chain. Claim we never saw any blocks. buf.write(new VarInt(subVerBytes.length).encode());
Utils.uint32ToByteStreamLE(0, buf); buf.write(subVerBytes);
// Size of known block chain.
Utils.uint32ToByteStreamLE(bestHeight, buf);
} }
/** /**

View File

@@ -22,6 +22,8 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.InetAddress; 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 * 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 class PingService {
public static void main(String[] args) throws Exception { 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. // Try to read the wallet from storage, create a new one if not possible.
Wallet wallet; Wallet wallet;
@@ -51,7 +53,6 @@ public class PingService {
BlockChain chain = new BlockChain(params, wallet); BlockChain chain = new BlockChain(params, wallet);
final Peer peer = new Peer(params, conn, chain); final Peer peer = new Peer(params, conn, chain);
peer.start(); peer.start();
peer.startBlockChainDownload().await();
// We want to know when the balance changes. // We want to know when the balance changes.
wallet.addEventListener(new WalletEventListener() { 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("Send coins to: " + key.toAddress(params).toString());
System.out.println("Waiting for coins to arrive. Press Ctrl-C to quit."); System.out.println("Waiting for coins to arrive. Press Ctrl-C to quit.");
// The peer thread keeps us alive until something kills the process. // The peer thread keeps us alive until something kills the process.