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.");
addresses = new ArrayList<PeerAddress>((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();
}

View File

@ -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()

View File

@ -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.
}
}
}

View File

@ -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;
}
}

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

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 {
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;
}

View File

@ -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 {

View File

@ -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);
}
/**

View File

@ -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.