3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 14:54:15 +00:00

API for setting version messages on outbound connections, and implementing BIP 14. Resolves issue 105.

This commit is contained in:
Mike Hearn 2012-02-02 17:34:52 +01:00
parent 7f82613559
commit b72c170086
8 changed files with 174 additions and 31 deletions

View File

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

View File

@ -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<GetDataFuture<Block>>();
this.eventListeners = new ArrayList<PeerEventListener>();
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) {

View File

@ -31,16 +31,16 @@ import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Maintain a number of connections to peers.
* <p/>
* <p>PeerGroup tries to maintain a constant number of connections to a set of distinct peers.
* Maintain a number of connections to peers.<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
* will be tried after a delay as long as the number of connections less than the maximum.<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>
*
* 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<PeerEventListener> peerEventListeners;
// Peer discovery sources, will be polled occasionally if there aren't enough inactives.
private Set<PeerDiscovery> 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<Wallet>(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<PeerAddress>();
// TODO: Remove usage of synchronized sets here in favor of simple coarse-grained locking.
peers = Collections.synchronizedSet(new HashSet<Peer>());
@ -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
* <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 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);
}
/**
* <p>Adds a listener that will be notified on a library controlled thread when:</p>
* <ol>
@ -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) {

View File

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

View File

@ -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.<p>
*
* 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.<p>
*
* 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. <p>
*
* Anything put in the "comments" field will appear in brackets and may be used for platform info, or anything
* else. For example, calling <tt>appendToSubVer("MultiBit", "1.0", "Windows")</tt> 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.<p>
*
* See <a href="https://en.bitcoin.it/wiki/BIP_0014">BIP 14</a> 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");
}
}

View File

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

View File

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

View File

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