Rewrite the network stack.

Remove Netty entirely, using the new Nio wrapper classes instead

* BitcoinSerializer now uses ByteBuffers directly instead of
  InputStreams.
* TCPNetworkConnection and NetworkConnection interface deleted,
  Peer now extends the abstract class PeerSocketHandler which
  handles deserialization and interfaces with the Nio wrapper
  classes.
* As a part of this, all version message handling has been moved
  to Peer, instead of doing it in TCPNetworkConnection.
* Peer.setMinProtocolVersion() now returns a boolean instead of a
  null/non-null future which holds the now-closing channel.
* Peer.sendMessage (now PeerSocketHandler.sendMessage()) now
  returns void.
* PeerGroup has some significant API changes:
  * removed constructors which take pipeline factories,
    makePipelineFactory, createClientBootstrap
  * Replaced with a setSocketTimeoutMillis method that sets a
    timeout between openConnection() and version/verack exchange.
    (Note that because Peer extends AbstractTimeoutHandler, it has
    useful timeout setters public already).
  * connectTo returns a Peer future, not a ChannelFuture
  * removed peerFromChannelFuture and peerFromChannel
* Peer and PeerGroup Tests have semi-significant rewrites:
  * They use actual TCP connections to localhost
  * The "remote" side is a InboundMessageQueuer, which queues
    inbound messages and allows for writing arbitrary messages.
  * It ignores certain special pings which come from pingAndWait,
    which is used to wait for message processing in the Peer.
  * Removed a broken test in PeerGroupTest that should be reenabled
    if we ever prefer a different version than our minimum version
    again.
  * Removed two duplicate tests in PeerTest (testRun_*Exception)
    which are tested for in badMessage as well.
  * Added a test for peer timeout and large message deserialization

Author:    Matt Corallo <git@bluematt.me>
This commit is contained in:
Matt Corallo
2013-07-16 20:07:04 +02:00
committed by Mike Hearn
parent 81f8b230e3
commit 534cec9791
44 changed files with 2138 additions and 1801 deletions

View File

@@ -192,12 +192,6 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty</artifactId>
<version>3.6.3.Final</version>
</dependency>
<dependency>
<groupId>com.madgag</groupId>
<artifactId>sc-light-jdk15on</artifactId>

View File

@@ -21,9 +21,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
@@ -129,9 +130,9 @@ public class BitcoinSerializer {
}
/**
* Reads a message from the given InputStream and returns it.
* Reads a message from the given ByteBuffer and returns it.
*/
public Message deserialize(InputStream in) throws ProtocolException, IOException {
public Message deserialize(ByteBuffer in) throws ProtocolException, IOException {
// A Bitcoin protocol message has the following format.
//
// - 4 byte magic number: 0xfabfb5da for the testnet or
@@ -156,7 +157,7 @@ public class BitcoinSerializer {
* Deserializes only the header in case packet meta data is needed before decoding
* the payload. This method assumes you have already called seekPastMagicBytes()
*/
public BitcoinPacketHeader deserializeHeader(InputStream in) throws ProtocolException, IOException {
public BitcoinPacketHeader deserializeHeader(ByteBuffer in) throws ProtocolException, IOException {
return new BitcoinPacketHeader(in);
}
@@ -164,16 +165,9 @@ public class BitcoinSerializer {
* Deserialize payload only. You must provide a header, typically obtained by calling
* {@link BitcoinSerializer#deserializeHeader}.
*/
public Message deserializePayload(BitcoinPacketHeader header, InputStream in) throws ProtocolException, IOException {
int readCursor = 0;
public Message deserializePayload(BitcoinPacketHeader header, ByteBuffer in) throws ProtocolException, BufferUnderflowException {
byte[] payloadBytes = new byte[header.size];
while (readCursor < payloadBytes.length - 1) {
int bytesRead = in.read(payloadBytes, readCursor, header.size - readCursor);
if (bytesRead == -1) {
throw new IOException("Socket is disconnected");
}
readCursor += bytesRead;
}
in.get(payloadBytes, 0, header.size);
// Verify the checksum.
byte[] hash;
@@ -246,17 +240,13 @@ public class BitcoinSerializer {
return message;
}
public void seekPastMagicBytes(InputStream in) throws IOException {
public void seekPastMagicBytes(ByteBuffer in) throws BufferUnderflowException {
int magicCursor = 3; // Which byte of the magic we're looking for currently.
while (true) {
int b = in.read(); // Read a byte.
if (b == -1) {
// There's no more data to read.
throw new IOException("Socket is disconnected");
}
byte b = in.get();
// We're looking for a run of bytes that is the same as the packet magic but we want to ignore partial
// magics that aren't complete. So we keep track of where we're up to with magicCursor.
int expectedByte = 0xFF & (int) (params.getPacketMagic() >>> (magicCursor * 8));
byte expectedByte = (byte)(0xFF & params.getPacketMagic() >>> (magicCursor * 8));
if (b == expectedByte) {
magicCursor--;
if (magicCursor < 0) {
@@ -287,22 +277,17 @@ public class BitcoinSerializer {
public static class BitcoinPacketHeader {
/** The largest number of bytes that a header can represent */
public static final int HEADER_LENGTH = COMMAND_LEN + 4 + 4;
public final byte[] header;
public final String command;
public final int size;
public final byte[] checksum;
public BitcoinPacketHeader(InputStream in) throws ProtocolException, IOException {
header = new byte[COMMAND_LEN + 4 + 4];
int readCursor = 0;
while (readCursor < header.length) {
int bytesRead = in.read(header, readCursor, header.length - readCursor);
if (bytesRead == -1) {
// There's no more data to read.
throw new IOException("Incomplete packet in underlying stream");
}
readCursor += bytesRead;
}
public BitcoinPacketHeader(ByteBuffer in) throws ProtocolException, BufferUnderflowException {
header = new byte[HEADER_LENGTH];
in.get(header, 0, header.length);
int cursor = 0;

View File

@@ -1,66 +0,0 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.core;
import java.io.IOException;
/**
* <p>A NetworkConnection handles talking to a remote Bitcoin peer at a low level. It understands how to read and write
* messages, but doesn't asynchronously communicate with the peer or handle the higher level details
* of the protocol. A NetworkConnection is typically stateless, so after constructing a NetworkConnection, give it to a
* newly created {@link Peer} to handle messages to and from that specific peer.</p>
*
* <p>If you just want to "get on the network" and don't care about the details, you want to use a {@link PeerGroup}
* instead. A {@link PeerGroup} handles the process of setting up connections to multiple peers, running background threads
* for them, and many other things.</p>
*
* <p>NetworkConnection is an interface in order to support multiple low level protocols. You likely want a
* {@link TCPNetworkConnection} as it's currently the only NetworkConnection implementation. In future there may be
* others that support connections over Bluetooth, NFC, UNIX domain sockets and so on.</p>
*/
public interface NetworkConnection {
/**
* Sends a "ping" message to the remote node. The protocol doesn't presently use this feature much.
*
* @throws IOException
*/
public void ping() throws IOException;
/**
* Writes the given message out over the network using the protocol tag. For a Transaction
* this should be "tx" for example. It's safe to call this from multiple threads simultaneously,
* the actual writing will be serialized.
*
* @throws IOException
*/
public void writeMessage(Message message) throws IOException;
/**
* Returns the version message received from the other end of the connection during the handshake.
*/
public VersionMessage getVersionMessage();
/**
* @return The address of the other side of the network connection.
*/
public PeerAddress getPeerAddress();
/**
* Does whatever needed to clean up the given connection, if necessary.
*/
public void close();
}

View File

@@ -16,10 +16,7 @@
package com.google.bitcoin.core;
import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.params.TestNet2Params;
import com.google.bitcoin.params.TestNet3Params;
import com.google.bitcoin.params.UnitTestParams;
import com.google.bitcoin.params.*;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.script.ScriptOpCodes;
import com.google.common.base.Objects;
@@ -162,6 +159,12 @@ public abstract class NetworkParameters implements Serializable {
return UnitTestParams.get();
}
/** Returns a standard regression test params (similar to unitTests) */
@Deprecated
public static NetworkParameters regTests() {
return RegTestParams.get();
}
/**
* A Java package style string acting as unique ID for these parameters
*/

View File

@@ -28,13 +28,10 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import net.jcip.annotations.GuardedBy;
import org.jboss.netty.channel.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -47,27 +44,37 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* A Peer handles the high level communication with a Bitcoin node.
* <p>A Peer handles the high level communication with a Bitcoin node, extending a {@link PeerSocketHandler} which
* handles low-level message (de)serialization.</p>
*
* <p>{@link Peer#getHandler()} is part of a Netty Pipeline with a Bitcoin serializer downstream of it.
* <p>Note that timeouts are handled by the extended
* {@link com.google.bitcoin.networkabstraction.AbstractTimeoutHandler} and timeout is automatically disabled (using
* {@link com.google.bitcoin.networkabstraction.AbstractTimeoutHandler#setTimeoutEnabled(boolean)}) once the version
* handshake completes.</p>
*/
public class Peer {
interface PeerLifecycleListener {
/** Called when the peer is connected */
public void onPeerConnected(Peer peer);
/** Called when the peer is disconnected */
public void onPeerDisconnected(Peer peer);
}
public class Peer extends PeerSocketHandler {
private static final Logger log = LoggerFactory.getLogger(Peer.class);
protected final ReentrantLock lock = Threading.lock("peer");
private final NetworkParameters params;
private final AbstractBlockChain blockChain;
private volatile PeerAddress vAddress;
private final CopyOnWriteArrayList<ListenerRegistration<PeerEventListener>> eventListeners;
private final CopyOnWriteArrayList<PeerLifecycleListener> lifecycleListeners;
// onPeerDisconnected should not be called directly by Peers when a PeerGroup is involved (we don't know the total
// number of connected peers), thus we use a wrapper that PeerGroup can use to register listeners that wont get
// onPeerDisconnected calls
static class PeerListenerRegistration extends ListenerRegistration<PeerEventListener> {
boolean callOnDisconnect = true;
public PeerListenerRegistration(PeerEventListener listener, Executor executor) {
super(listener, executor);
}
public PeerListenerRegistration(PeerEventListener listener, Executor executor, boolean callOnDisconnect) {
this(listener, executor);
this.callOnDisconnect = callOnDisconnect;
}
}
private final CopyOnWriteArrayList<PeerListenerRegistration> eventListeners;
// Whether to try and download blocks and transactions from this peer. Set to false by PeerGroup if not the
// primary peer. This is to avoid redundant work and concurrency problems with downloading the same chain
// in parallel.
@@ -130,46 +137,74 @@ public class Peer {
private final CopyOnWriteArrayList<PendingPing> pendingPings;
private static final int PING_MOVING_AVERAGE_WINDOW = 20;
private volatile Channel vChannel;
private volatile VersionMessage vPeerVersionMessage;
private boolean isAcked;
private final PeerHandler handler;
// A settable future which completes (with this) when the connection is open
private final SettableFuture<Peer> connectionOpenFuture = SettableFuture.create();
/**
* Construct a peer that reads/writes from the given block chain.
* <p>Construct a peer that reads/writes from the given block chain.</p>
*
* <p>Note that this does <b>NOT</b> make a connection to the given remoteAddress, it only creates a handler for a
* connection. If you want to create a one-off connection, create a Peer and pass it to
* {@link com.google.bitcoin.networkabstraction.NioClientManager#openConnection(java.net.SocketAddress, com.google.bitcoin.networkabstraction.StreamParser)}
* or
* {@link com.google.bitcoin.networkabstraction.NioClient#NioClient(java.net.SocketAddress, com.google.bitcoin.networkabstraction.StreamParser, int)}.</p>
*
* <p>The remoteAddress provided should match the remote address of the peer which is being connected to, and is
* used to keep track of which peers relayed transactions and offer more descriptive logging.</p>
*/
public Peer(NetworkParameters params, AbstractBlockChain chain, VersionMessage ver) {
this(params, chain, ver, null);
public Peer(NetworkParameters params, VersionMessage ver, @Nullable AbstractBlockChain chain, InetSocketAddress remoteAddress) {
this(params, ver, remoteAddress, chain, null);
}
/**
* Construct a peer that reads/writes from the given block chain and memory pool. Transactions stored
* in a memory pool will have their confidence levels updated when a peer announces it, to reflect the greater
* likelyhood that the transaction is valid.
* <p>Construct a peer that reads/writes from the given block chain and memory pool. Transactions stored in a memory
* pool will have their confidence levels updated when a peer announces it, to reflect the greater likelyhood that
* the transaction is valid.</p>
*
* <p>Note that this does <b>NOT</b> make a connection to the given remoteAddress, it only creates a handler for a
* connection. If you want to create a one-off connection, create a Peer and pass it to
* {@link com.google.bitcoin.networkabstraction.NioClientManager#openConnection(java.net.SocketAddress, com.google.bitcoin.networkabstraction.StreamParser)}
* or
* {@link com.google.bitcoin.networkabstraction.NioClient#NioClient(java.net.SocketAddress, com.google.bitcoin.networkabstraction.StreamParser, int)}.</p>
*
* <p>The remoteAddress provided should match the remote address of the peer which is being connected to, and is
* used to keep track of which peers relayed transactions and offer more descriptive logging.</p>
*/
public Peer(NetworkParameters params, @Nullable AbstractBlockChain chain, VersionMessage ver, @Nullable MemoryPool mempool) {
public Peer(NetworkParameters params, VersionMessage ver, InetSocketAddress remoteAddress,
@Nullable AbstractBlockChain chain, @Nullable MemoryPool mempool) {
super(params, remoteAddress);
this.params = Preconditions.checkNotNull(params);
this.versionMessage = Preconditions.checkNotNull(ver);
this.blockChain = chain; // Allowed to be null.
this.vDownloadData = chain != null;
this.getDataFutures = new CopyOnWriteArrayList<GetDataRequest>();
this.eventListeners = new CopyOnWriteArrayList<ListenerRegistration<PeerEventListener>>();
this.lifecycleListeners = new CopyOnWriteArrayList<PeerLifecycleListener>();
this.eventListeners = new CopyOnWriteArrayList<PeerListenerRegistration>();
this.fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds();
this.isAcked = false;
this.handler = new PeerHandler();
this.pendingPings = new CopyOnWriteArrayList<PendingPing>();
this.wallets = new CopyOnWriteArrayList<Wallet>();
this.memoryPool = mempool;
}
/**
* Construct a peer that reads/writes from the given chain. Automatically creates a VersionMessage for you from the
* given software name/version strings, which should be something like "MySimpleTool", "1.0" and which will tell the
* remote node to relay transaction inv messages before it has received a filter.
* <p>Construct a peer that reads/writes from the given chain. Automatically creates a VersionMessage for you from
* the given software name/version strings, which should be something like "MySimpleTool", "1.0" and which will tell
* the remote node to relay transaction inv messages before it has received a filter.</p>
*
* <p>Note that this does <b>NOT</b> make a connection to the given remoteAddress, it only creates a handler for a
* connection. If you want to create a one-off connection, create a Peer and pass it to
* {@link com.google.bitcoin.networkabstraction.NioClientManager#openConnection(java.net.SocketAddress, com.google.bitcoin.networkabstraction.StreamParser)}
* or
* {@link com.google.bitcoin.networkabstraction.NioClient#NioClient(java.net.SocketAddress, com.google.bitcoin.networkabstraction.StreamParser, int)}.</p>
*
* <p>The remoteAddress provided should match the remote address of the peer which is being connected to, and is
* used to keep track of which peers relayed transactions and offer more descriptive logging.</p>
*/
public Peer(NetworkParameters params, AbstractBlockChain blockChain, String thisSoftwareName, String thisSoftwareVersion) {
this(params, blockChain, new VersionMessage(params, blockChain.getBestChainHeight(), true));
public Peer(NetworkParameters params, AbstractBlockChain blockChain, InetSocketAddress remoteAddress, String thisSoftwareName, String thisSoftwareVersion) {
this(params, new VersionMessage(params, blockChain.getBestChainHeight(), true), blockChain, remoteAddress);
this.versionMessage.appendToSubVer(thisSoftwareName, thisSoftwareVersion, null);
}
@@ -191,24 +226,21 @@ public class Peer {
* threads in order to get the results of those hook methods.
*/
public void addEventListener(PeerEventListener listener, Executor executor) {
eventListeners.add(new ListenerRegistration<PeerEventListener>(listener, executor));
eventListeners.add(new PeerListenerRegistration(listener, executor));
}
// Package-local version for PeerGroup
void addEventListenerWithoutOnDisconnect(PeerEventListener listener, Executor executor) {
eventListeners.add(new PeerListenerRegistration(listener, executor, false));
}
public boolean removeEventListener(PeerEventListener listener) {
return ListenerRegistration.removeFromList(listener, eventListeners);
}
void addLifecycleListener(PeerLifecycleListener listener) {
lifecycleListeners.add(listener);
}
boolean removeLifecycleListener(PeerLifecycleListener listener) {
return lifecycleListeners.remove(listener);
}
@Override
public String toString() {
PeerAddress addr = vAddress;
PeerAddress addr = getAddress();
if (addr == null) {
// User-provided NetworkConnection object.
return "Peer()";
@@ -217,59 +249,40 @@ public class Peer {
}
}
private void notifyDisconnect() {
for (PeerLifecycleListener listener : lifecycleListeners) {
listener.onPeerDisconnected(Peer.this);
@Override
public void connectionClosed() {
for (final PeerListenerRegistration registration : eventListeners) {
if (registration.callOnDisconnect)
registration.executor.execute(new Runnable() {
@Override
public void run() {
registration.listener.onPeerDisconnected(Peer.this, 0);
}
});
}
}
class PeerHandler extends SimpleChannelHandler {
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
super.channelClosed(ctx, e);
notifyDisconnect();
}
@Override
public void connectRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
vAddress = new PeerAddress((InetSocketAddress)e.getValue());
vChannel = e.getChannel();
super.connectRequested(ctx, e);
}
/** Catch any exceptions, logging them and then closing the channel. */
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
String s;
PeerAddress addr = vAddress;
s = addr == null ? "?" : addr.toString();
final Throwable cause = e.getCause();
if (cause instanceof ConnectException || cause instanceof IOException) {
// Short message for network errors
log.info(s + " - " + cause.getMessage());
} else {
log.warn(s + " - ", cause);
Thread.UncaughtExceptionHandler handler = Threading.uncaughtExceptionHandler;
if (handler != null)
handler.uncaughtException(Thread.currentThread(), cause);
}
e.getChannel().close();
}
/** Handle incoming Bitcoin messages */
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Message m = (Message)e.getMessage();
processMessage(e, m);
}
public Peer getPeer() {
return Peer.this;
}
@Override
public void connectionOpened() {
// Announce ourselves. This has to come first to connect to clients beyond v0.3.20.2 which wait to hear
// from us until they send their version message back.
PeerAddress address = getAddress();
log.info("Announcing to {} as: {}", address == null ? "Peer" : address.toSocketAddress(), versionMessage.subVer);
sendMessage(versionMessage);
connectionOpenFuture.set(this);
// 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.
}
private void processMessage(MessageEvent e, Message m) throws Exception {
/**
* Provides a ListenableFuture that can be used to wait for the socket to connect. A socket connection does not
* mean that protocol handshake has occurred.
*/
public ListenableFuture<Peer> getConnectionOpenFuture() {
return connectionOpenFuture;
}
protected void processMessage(Message m) throws Exception {
// Allow event listeners to filter the message stream. Listeners are allowed to drop messages by
// returning null.
for (ListenerRegistration<PeerEventListener> registration : eventListeners) {
@@ -312,7 +325,7 @@ public class Peer {
} else if (m instanceof AlertMessage) {
processAlert((AlertMessage) m);
} else if (m instanceof VersionMessage) {
vPeerVersionMessage = (VersionMessage) m;
processVersionMessage((VersionMessage) m);
} else if (m instanceof VersionAck) {
if (vPeerVersionMessage == null) {
throw new ProtocolException("got a version ack before version");
@@ -321,15 +334,22 @@ public class Peer {
throw new ProtocolException("got more than one version ack");
}
isAcked = true;
for (PeerLifecycleListener listener : lifecycleListeners)
listener.onPeerConnected(this);
this.setTimeoutEnabled(false);
for (final ListenerRegistration<PeerEventListener> registration : eventListeners) {
registration.executor.execute(new Runnable() {
@Override
public void run() {
registration.listener.onPeerConnected(Peer.this, 1);
}
});
}
// We check min version after onPeerConnected as channel.close() will
// call onPeerDisconnected, and we should probably call onPeerConnected first.
final int version = vMinProtocolVersion;
if (vPeerVersionMessage.clientVersion < version) {
log.warn("Connected to a peer speaking protocol version {} but need {}, closing",
vPeerVersionMessage.clientVersion, version);
e.getChannel().close();
close();
}
} else if (m instanceof Ping) {
if (((Ping) m).hasNonce())
@@ -341,7 +361,36 @@ public class Peer {
}
}
private void startFilteredBlock(FilteredBlock m) throws IOException {
private void processVersionMessage(VersionMessage m) throws ProtocolException {
if (vPeerVersionMessage != null)
throw new ProtocolException("Got two version messages from peer");
vPeerVersionMessage = m;
// Switch to the new protocol version.
int peerVersion = vPeerVersionMessage.clientVersion;
PeerAddress peerAddress = getAddress();
log.info("Connected to {}: version={}, subVer='{}', services=0x{}, time={}, blocks={}", new Object[] {
peerAddress == null ? "Peer" : peerAddress.getAddr().getHostAddress(),
peerVersion,
vPeerVersionMessage.subVer,
vPeerVersionMessage.localServices,
new Date(vPeerVersionMessage.time * 1000),
vPeerVersionMessage.bestHeight
});
// Now it's our turn ...
// Send an ACK message stating we accept the peers protocol version.
sendMessage(new VersionAck());
// 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. Some bogus
// implementations claim to have a block chain in their services field but then report a height of zero, filter
// them out here.
if (!vPeerVersionMessage.hasBlockChain() ||
(!params.allowEmptyPeerChain() && vPeerVersionMessage.bestHeight <= 0)) {
// Shut down the channel
throw new ProtocolException("Peer does not have a copy of the block chain.");
}
}
private void startFilteredBlock(FilteredBlock m) {
// Filtered blocks come before the data that they refer to, so stash it here and then fill it out as
// messages stream in. We'll call endFilteredBlock when a non-tx message arrives (eg, another
// FilteredBlock) or when a tx that isn't needed by that block is found. A ping message is sent after
@@ -390,12 +439,7 @@ public class Peer {
}
}
/** Returns the Netty Pipeline stage handling the high level Bitcoin protocol. */
public PeerHandler getHandler() {
return handler;
}
private void processHeaders(HeadersMessage m) throws IOException, ProtocolException {
private void processHeaders(HeadersMessage m) throws ProtocolException {
// Runs in network loop thread for this peer.
//
// This method can run if a peer just randomly sends us a "headers" message (should never happen), or more
@@ -475,8 +519,8 @@ public class Peer {
}
}
private void processGetData(GetDataMessage getdata) throws IOException {
log.info("{}: Received getdata message: {}", vAddress, getdata.toString());
private void processGetData(GetDataMessage getdata) {
log.info("{}: Received getdata message: {}", getAddress(), getdata.toString());
ArrayList<Message> items = new ArrayList<Message>();
for (ListenerRegistration<PeerEventListener> registration : eventListeners) {
if (registration.executor != Threading.SAME_THREAD) continue;
@@ -487,19 +531,19 @@ public class Peer {
if (items.size() == 0) {
return;
}
log.info("{}: Sending {} items gathered from listeners to peer", vAddress, items.size());
log.info("{}: Sending {} items gathered from listeners to peer", getAddress(), items.size());
for (Message item : items) {
sendMessage(item);
}
}
private void processTransaction(Transaction tx) throws VerificationException, IOException {
private void processTransaction(Transaction tx) throws VerificationException {
// Check a few basic syntax issues to ensure the received TX isn't nonsense.
tx.verify();
final Transaction fTx;
lock.lock();
try {
log.debug("{}: Received tx {}", vAddress, tx.getHashAsString());
log.debug("{}: Received tx {}", getAddress(), tx.getHashAsString());
if (memoryPool != null) {
// We may get back a different transaction object.
tx = memoryPool.seen(tx, getAddress());
@@ -537,11 +581,11 @@ public class Peer {
Futures.addCallback(downloadDependencies(fTx), new FutureCallback<List<Transaction>>() {
public void onSuccess(List<Transaction> dependencies) {
try {
log.info("{}: Dependency download complete!", vAddress);
log.info("{}: Dependency download complete!", getAddress());
wallet.receivePending(fTx, dependencies);
} catch (VerificationException e) {
log.error("{}: Wallet failed to process pending transaction {}",
vAddress, fTx.getHashAsString());
getAddress(), fTx.getHashAsString());
log.error("Error was: ", e);
// Not much more we can do at this point.
}
@@ -595,7 +639,7 @@ public class Peer {
checkNotNull(memoryPool, "Must have a configured MemoryPool object to download dependencies.");
TransactionConfidence.ConfidenceType txConfidence = tx.getConfidence().getConfidenceType();
Preconditions.checkArgument(txConfidence != TransactionConfidence.ConfidenceType.BUILDING);
log.info("{}: Downloading dependencies of {}", vAddress, tx.getHashAsString());
log.info("{}: Downloading dependencies of {}", getAddress(), tx.getHashAsString());
final LinkedList<Transaction> results = new LinkedList<Transaction>();
// future will be invoked when the entire dependency tree has been walked and the results compiled.
final ListenableFuture future = downloadDependenciesInternal(tx, new Object(), results);
@@ -646,7 +690,7 @@ public class Peer {
GetDataMessage getdata = new GetDataMessage(params);
final long nonce = (long)(Math.random()*Long.MAX_VALUE);
if (needToRequest.size() > 1)
log.info("{}: Requesting {} transactions for dep resolution", vAddress, needToRequest.size());
log.info("{}: Requesting {} transactions for dep resolution", getAddress(), needToRequest.size());
for (Sha256Hash hash : needToRequest) {
getdata.addTransaction(hash);
GetDataRequest req = new GetDataRequest();
@@ -670,7 +714,7 @@ public class Peer {
List<ListenableFuture<Object>> childFutures = Lists.newLinkedList();
for (Transaction tx : transactions) {
if (tx == null) continue;
log.info("{}: Downloaded dependency of {}: {}", vAddress, rootTxHash, tx.getHashAsString());
log.info("{}: Downloaded dependency of {}: {}", getAddress(), rootTxHash, tx.getHashAsString());
results.add(tx);
// Now recurse into the dependencies of this transaction too.
childFutures.add(downloadDependenciesInternal(tx, marker, results));
@@ -727,9 +771,9 @@ public class Peer {
return resultFuture;
}
private void processBlock(Block m) throws IOException {
private void processBlock(Block m) {
if (log.isDebugEnabled()) {
log.debug("{}: Received broadcast block {}", vAddress, m.getHashAsString());
log.debug("{}: Received broadcast block {}", getAddress(), m.getHashAsString());
}
// Was this block requested by getBlock()?
if (maybeHandleRequestedData(m)) return;
@@ -739,7 +783,7 @@ public class Peer {
}
// Did we lose download peer status after requesting block data?
if (!vDownloadData) {
log.debug("{}: Received block we did not ask for: {}", vAddress, m.getHashAsString());
log.debug("{}: Received block we did not ask for: {}", getAddress(), m.getHashAsString());
return;
}
pendingBlockDownloads.remove(m.getHash());
@@ -781,7 +825,7 @@ public class Peer {
}
} catch (VerificationException e) {
// We don't want verification failures to kill the thread.
log.warn("{}: Block verification failed", vAddress, e);
log.warn("{}: Block verification failed", getAddress(), e);
} catch (PrunedException e) {
// Unreachable when in SPV mode.
throw new RuntimeException(e);
@@ -789,12 +833,12 @@ public class Peer {
}
// TODO: Fix this duplication.
private void endFilteredBlock(FilteredBlock m) throws IOException {
private void endFilteredBlock(FilteredBlock m) {
if (log.isDebugEnabled()) {
log.debug("{}: Received broadcast filtered block {}", vAddress, m.getHash().toString());
log.debug("{}: Received broadcast filtered block {}", getAddress(), m.getHash().toString());
}
if (!vDownloadData) {
log.debug("{}: Received block we did not ask for: {}", vAddress, m.getHash().toString());
log.debug("{}: Received block we did not ask for: {}", getAddress(), m.getHash().toString());
return;
}
if (blockChain == null) {
@@ -850,7 +894,7 @@ public class Peer {
}
} catch (VerificationException e) {
// We don't want verification failures to kill the thread.
log.warn("{}: FilteredBlock verification failed", vAddress, e);
log.warn("{}: FilteredBlock verification failed", getAddress(), e);
} catch (PrunedException e) {
// We pruned away some of the data we need to properly handle this block. We need to request the needed
// data from the remote peer and fix things. Or just give up.
@@ -888,7 +932,7 @@ public class Peer {
}
}
private void processInv(InventoryMessage inv) throws IOException {
private void processInv(InventoryMessage inv) {
List<InventoryItem> items = inv.getItems();
// Separate out the blocks and transactions, we'll handle them differently
@@ -945,7 +989,7 @@ public class Peer {
// Some other peer already announced this so don't download.
it.remove();
} else {
log.debug("{}: getdata on tx {}", vAddress, item.hash);
log.debug("{}: getdata on tx {}", getAddress(), item.hash);
getdata.addItem(item);
}
// This can trigger transaction confidence listeners.
@@ -1017,7 +1061,7 @@ public class Peer {
* If you want the block right away and don't mind waiting for it, just call .get() on the result. Your thread
* will block until the peer answers.
*/
public ListenableFuture<Block> getBlock(Sha256Hash blockHash) throws IOException {
public ListenableFuture<Block> getBlock(Sha256Hash blockHash) {
// This does not need to be locked.
log.info("Request to fetch block {}", blockHash);
GetDataMessage getdata = new GetDataMessage(params);
@@ -1030,7 +1074,7 @@ public class Peer {
* retrieved this way because peers don't have a transaction ID to transaction-pos-on-disk index, and besides,
* in future many peers will delete old transaction data they don't need.
*/
public ListenableFuture<Transaction> getPeerMempoolTransaction(Sha256Hash hash) throws IOException {
public ListenableFuture<Transaction> getPeerMempoolTransaction(Sha256Hash hash) {
// This does not need to be locked.
// TODO: Unit test this method.
log.info("Request to fetch peer mempool tx {}", hash);
@@ -1040,7 +1084,7 @@ public class Peer {
}
/** Sends a getdata with a single item in it. */
private ListenableFuture sendSingleGetData(GetDataMessage getdata) throws IOException {
private ListenableFuture sendSingleGetData(GetDataMessage getdata) {
// This does not need to be locked.
Preconditions.checkArgument(getdata.getItems().size() == 1);
GetDataRequest req = new GetDataRequest();
@@ -1095,21 +1139,13 @@ public class Peer {
wallets.remove(wallet);
}
/**
* Sends the given message on the peers Channel.
*/
public ChannelFuture sendMessage(Message m) {
// This does not need to be locked.
return Channels.write(vChannel, m);
}
// Keep track of the last request we made to the peer in blockChainDownloadLocked so we can avoid redundant and harmful
// getblocks requests.
@GuardedBy("lock")
private Sha256Hash lastGetBlocksBegin, lastGetBlocksEnd;
@GuardedBy("lock")
private void blockChainDownloadLocked(Sha256Hash toHash) throws IOException {
private void blockChainDownloadLocked(Sha256Hash toHash) {
checkState(lock.isHeldByCurrentThread());
// The block chain download process is a bit complicated. Basically, we start with one or more blocks in a
// chain that we have from a previous session. We want to catch up to the head of the chain BUT we don't know
@@ -1197,7 +1233,7 @@ public class Peer {
* 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.
*/
public void startBlockChainDownload() throws IOException {
public void startBlockChainDownload() {
setDownloadData(true);
// TODO: peer might still have blocks that we don't have, and even have a heavier
// chain even if the chain block count is lower.
@@ -1271,11 +1307,11 @@ public class Peer {
* updated.
* @throws ProtocolException if the peer version is too low to support measurable pings.
*/
public ListenableFuture<Long> ping() throws IOException, ProtocolException {
public ListenableFuture<Long> ping() throws ProtocolException {
return ping((long) (Math.random() * Long.MAX_VALUE));
}
protected ListenableFuture<Long> ping(long nonce) throws IOException, ProtocolException {
protected ListenableFuture<Long> ping(long nonce) throws ProtocolException {
final VersionMessage ver = vPeerVersionMessage;
if (!ver.isPingPongSupported())
throw new ProtocolException("Peer version is too low for measurable pings: " + ver);
@@ -1366,13 +1402,6 @@ public class Peer {
this.vDownloadData = downloadData;
}
/**
* @return the IP address and port of peer.
*/
public PeerAddress getAddress() {
return vAddress;
}
/** Returns version data announced by the remote peer. */
public VersionMessage getPeerVersionMessage() {
return vPeerVersionMessage;
@@ -1393,16 +1422,16 @@ public class Peer {
/**
* The minimum P2P protocol version that is accepted. If the peer speaks a protocol version lower than this, it
* will be disconnected.
* @return if not-null then this is the future for the Peer disconnection event.
* @return true if the peer was disconnected as a result
*/
@Nullable public ChannelFuture setMinProtocolVersion(int minProtocolVersion) {
public boolean setMinProtocolVersion(int minProtocolVersion) {
this.vMinProtocolVersion = minProtocolVersion;
if (getVersionMessage().clientVersion < minProtocolVersion) {
log.warn("{}: Disconnecting due to new min protocol version {}", this, minProtocolVersion);
return Channels.close(vChannel);
} else {
return null;
close();
return true;
}
return false;
}
/**
@@ -1420,7 +1449,7 @@ public class Peer {
* <p>If the remote peer doesn't support Bloom filtering, then this call is ignored. Once set you presently cannot
* unset a filter, though the underlying p2p protocol does support it.</p>
*/
public void setBloomFilter(BloomFilter filter) throws IOException {
public void setBloomFilter(BloomFilter filter) {
checkNotNull(filter, "Clearing filters is not currently supported");
final VersionMessage ver = vPeerVersionMessage;
if (ver == null || !ver.isBloomFilteringSupported())
@@ -1428,13 +1457,8 @@ public class Peer {
vBloomFilter = filter;
boolean shouldQueryMemPool = memoryPool != null || vDownloadData;
log.info("{}: Sending Bloom filter{}", this, shouldQueryMemPool ? " and querying mempool" : "");
ChannelFuture future = sendMessage(filter);
if (shouldQueryMemPool)
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
sendMessage(new MemoryPoolMessage());
}
});
sendMessage(filter);
sendMessage(new MemoryPoolMessage());
}
/**

View File

@@ -45,7 +45,7 @@ public interface PeerEventListener {
/**
* Called when a peer is connected. If this listener is registered to a {@link Peer} instead of a {@link PeerGroup},
* this will never be called.
* peerCount will always be 1.
*
* @param peer
* @param peerCount the total number of connected peers
@@ -55,7 +55,7 @@ public interface PeerEventListener {
/**
* Called when a peer is disconnected. Note that this won't be called if the listener is registered on a
* {@link PeerGroup} and the group is in the process of shutting down. If this listener is registered to a
* {@link Peer} instead of a {@link PeerGroup}, this will never be called.
* {@link Peer} instead of a {@link PeerGroup}, peerCount will always be 0.
*
* @param peer
* @param peerCount the total number of connected peers
@@ -79,8 +79,11 @@ public interface PeerEventListener {
public void onTransaction(Peer peer, Transaction t);
/**
* Called when a peer receives a getdata message, usually in response to an "inv" being broadcast. Return as many
* items as possible which appear in the {@link GetDataMessage}, or null if you're not interested in responding.
* <p>Called when a peer receives a getdata message, usually in response to an "inv" being broadcast. Return as many
* items as possible which appear in the {@link GetDataMessage}, or null if you're not interested in responding.</p>
*
* <p>Note that this will never be called if registered with any executor other than
* {@link com.google.bitcoin.utils.Threading#SAME_THREAD}</p>
*/
public List<Message> getData(Peer peer, GetDataMessage m);
}

View File

@@ -17,30 +17,24 @@
package com.google.bitcoin.core;
import com.google.bitcoin.core.Peer.PeerHandler;
import com.google.bitcoin.discovery.PeerDiscovery;
import com.google.bitcoin.discovery.PeerDiscoveryException;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.networkabstraction.ClientConnectionManager;
import com.google.bitcoin.networkabstraction.NioClientManager;
import com.google.bitcoin.utils.ListenerRegistration;
import com.google.bitcoin.utils.Threading;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.*;
import net.jcip.annotations.GuardedBy;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.*;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@@ -83,7 +77,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
private final CopyOnWriteArrayList<Peer> peers;
// Currently connecting peers.
private final CopyOnWriteArrayList<Peer> pendingPeers;
private final ChannelGroup channels;
private final ClientConnectionManager channels;
// The peer that has been selected for the purposes of downloading announced data.
@GuardedBy("lock") private Peer downloadPeer;
@@ -126,7 +120,6 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
}
};
private ClientBootstrap bootstrap;
private int minBroadcastConnections = 0;
private AbstractWalletEventListener walletEventListener = new AbstractWalletEventListener() {
private void onChanged() {
@@ -138,19 +131,21 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
@Override public void onCoinsSent(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { onChanged(); }
};
private class PeerStartupListener implements Peer.PeerLifecycleListener {
public void onPeerConnected(Peer peer) {
private class PeerStartupListener extends AbstractPeerEventListener {
@Override
public void onPeerConnected(Peer peer, int peerCount) {
handleNewPeer(peer);
}
public void onPeerDisconnected(Peer peer) {
@Override
public void onPeerDisconnected(Peer peer, int peerCount) {
// The channel will be automatically removed from channels.
handlePeerDeath(peer);
}
}
// Visible for testing
Peer.PeerLifecycleListener startupListener = new PeerStartupListener();
PeerEventListener startupListener = new PeerStartupListener();
// A bloom filter generated from all connected wallets that is given to new peers
private BloomFilter bloomFilter;
@@ -164,6 +159,10 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
private final long bloomFilterTweak = (long) (Math.random() * Long.MAX_VALUE);
private int lastBloomFilterElementCount;
/** The default timeout between when a connection attempt begins and version message exchange completes */
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 2000;
private volatile int vConnectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS;
/**
* Creates a PeerGroup with the given parameters. No chain is provided so this node will report its chain height
* as zero to other peers. This constructor is useful if you just want to explore the network but aren't interested
@@ -179,29 +178,15 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
* Creates a PeerGroup for the given network and chain. Blocks will be passed to the chain as they are broadcast
* and downloaded. This is probably the constructor you want to use.
*/
public PeerGroup(NetworkParameters params, AbstractBlockChain chain) {
this(params, chain, null);
public PeerGroup(NetworkParameters params, @Nullable AbstractBlockChain chain) {
this(params, chain, new NioClientManager());
}
/**
* <p>Creates a PeerGroup for the given network and chain, using the provided Netty {@link ClientBootstrap} object.
* </p>
*
* <p>A ClientBootstrap creates raw (TCP) connections to other nodes on the network. Normally you won't need to
* provide one - use the other constructors. Providing your own bootstrap is useful if you want to control
* details like how many network threads are used, the connection timeout value and so on. To do this, you can
* use {@link PeerGroup#createClientBootstrap()} method and then customize the resulting object. Example:</p>
*
* <pre>
* ClientBootstrap bootstrap = PeerGroup.createClientBootstrap();
* bootstrap.setOption("connectTimeoutMillis", 3000);
* PeerGroup peerGroup = new PeerGroup(params, chain, bootstrap);
* </pre>
*
* <p>The ClientBootstrap provided does not need a channel pipeline factory set. If one wasn't set, the provided
* bootstrap will be modified to have one that sets up the pipelines correctly.</p>
* Creates a new PeerGroup allowing you to specify the {@link ClientConnectionManager} which is used to create new
* connections and keep track of existing ones.
*/
public PeerGroup(NetworkParameters params, @Nullable AbstractBlockChain chain, @Nullable ClientBootstrap bootstrap) {
public PeerGroup(NetworkParameters params, @Nullable AbstractBlockChain chain, ClientConnectionManager connectionManager) {
this.params = checkNotNull(params);
this.chain = chain;
this.fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds();
@@ -219,64 +204,14 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
memoryPool = new MemoryPool();
// Configure Netty. The "ClientBootstrap" creates connections to other nodes. It can be configured in various
// ways to control the network.
if (bootstrap == null) {
this.bootstrap = createClientBootstrap();
this.bootstrap.setPipelineFactory(makePipelineFactory(params, chain));
} else {
this.bootstrap = bootstrap;
}
inactives = new ArrayList<PeerAddress>();
peers = new CopyOnWriteArrayList<Peer>();
pendingPeers = new CopyOnWriteArrayList<Peer>();
channels = new DefaultChannelGroup();
peerDiscoverers = new CopyOnWriteArraySet<PeerDiscovery>();
channels = connectionManager;
peerDiscoverers = new CopyOnWriteArraySet<PeerDiscovery>();
peerEventListeners = new CopyOnWriteArrayList<ListenerRegistration<PeerEventListener>>();
}
/**
* Helper method that just sets up a normal Netty ClientBootstrap using the default options, except for a custom
* thread factory that gives worker threads useful names and lowers their priority (to avoid competing with UI
* threads). You don't normally need to call this - if you aren't sure what it does, just use the regular
* constructors for {@link PeerGroup} that don't take a ClientBootstrap object.
*/
public static ClientBootstrap createClientBootstrap() {
ExecutorService bossExecutor = Executors.newCachedThreadPool(new PeerGroupThreadFactory());
ExecutorService workerExecutor = Executors.newCachedThreadPool(new PeerGroupThreadFactory());
NioClientSocketChannelFactory channelFactory = new NioClientSocketChannelFactory(bossExecutor, workerExecutor);
ClientBootstrap bs = new ClientBootstrap(channelFactory);
bs.setOption("connectTimeoutMillis", 2000);
return bs;
}
// Create a Netty pipeline factory. The pipeline factory will create a network processing
// pipeline with the bitcoin serializer ({@code TCPNetworkConnection}) downstream
// of the higher level {@code Peer}. Received packets will first be decoded, then passed
// {@code Peer}. Sent packets will be created by the {@code Peer}, then encoded and sent.
private ChannelPipelineFactory makePipelineFactory(final NetworkParameters params, @Nullable final AbstractBlockChain chain) {
return new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
// This runs unlocked.
VersionMessage ver = getVersionMessage().duplicate();
ver.bestHeight = chain == null ? 0 : chain.getBestChainHeight();
ver.time = Utils.now().getTime() / 1000;
ChannelPipeline p = Channels.pipeline();
Peer peer = new Peer(params, chain, ver, memoryPool);
peer.addLifecycleListener(startupListener);
peer.setMinProtocolVersion(vMinRequiredProtocolVersion);
pendingPeers.add(peer);
TCPNetworkConnection codec = new TCPNetworkConnection(params, peer.getVersionMessage());
p.addLast("codec", codec.getHandler());
p.addLast("peer", peer.getHandler());
return p;
}
};
}
/**
* Adjusts the desired number of connections that we will create to peers. Note that if there are already peers
* open and the new value is lower than the current number of peers, those connections will be terminated. Likewise
@@ -292,7 +227,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
lock.unlock();
}
// We may now have too many or too few open connections. Add more or drop some to get to the right amount.
adjustment = maxConnections - channels.size();
adjustment = maxConnections - channels.getConnectedClientCount();
while (adjustment > 0) {
try {
connectToAnyPeer();
@@ -301,10 +236,8 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
}
adjustment--;
}
while (adjustment < 0) {
channels.iterator().next().close();
adjustment++;
}
if (adjustment < 0)
channels.closeConnections(-adjustment);
}
/** The maximum number of connections that we will create to peers. */
@@ -576,6 +509,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
protected void startUp() throws Exception {
// This is run in a background thread by the AbstractIdleService implementation.
vPingTimer = new Timer("Peer pinging thread", true);
channels.startAndWait();
// Bring up the requested number of connections. If a connect attempt fails,
// new peers will be tried until there is a success, so just calling connectToAnyPeer for the wanted number
// of peers is sufficient.
@@ -593,11 +527,8 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
protected void shutDown() throws Exception {
// This is run on a separate thread by the AbstractIdleService implementation.
vPingTimer.cancel();
// Blocking close of all sockets. TODO: there is a race condition here, for the solution see:
// http://biasedbit.com/netty-releaseexternalresources-hangs/
channels.close().await();
// All thread pools should be stopped by this call.
bootstrap.releaseExternalResources();
// Blocking close of all sockets.
channels.stopAndWait();
for (PeerDiscovery peerDiscovery : peerDiscoverers) {
peerDiscovery.shutdown();
}
@@ -701,11 +632,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
if (!filter.equals(bloomFilter)) {
bloomFilter = filter;
for (Peer peer : peers)
try {
peer.setBloomFilter(filter);
} catch (IOException e) {
throw new RuntimeException(e);
}
peer.setBloomFilter(filter);
}
}
// Now adjust the earliest key time backwards by a week to handle the case of clock drift. This can occur
@@ -747,38 +674,32 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
}
/**
* Connect to a peer by creating a Netty channel to the destination address.
* Connect to a peer by creating a channel to the destination address.
*
* @param address destination IP and port.
* @return a ChannelFuture that can be used to wait for the socket to connect. A socket
* connection does not mean that protocol handshake has occured.
* @return The newly created Peer object. Use {@link com.google.bitcoin.core.Peer#getConnectionOpenFuture()} if you
* want a future which completes when the connection is open.
*/
public ChannelFuture connectTo(SocketAddress address) {
public Peer connectTo(InetSocketAddress address) {
return connectTo(address, true);
}
// Internal version.
protected ChannelFuture connectTo(SocketAddress address, boolean incrementMaxConnections) {
ChannelFuture future = bootstrap.connect(address);
// Make sure that the channel group gets access to the channel only if it connects successfully (otherwise
// it cannot be closed and trying to do so will cause problems).
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess())
channels.add(future.getChannel());
}
});
protected Peer connectTo(InetSocketAddress address, boolean incrementMaxConnections) {
VersionMessage ver = getVersionMessage().duplicate();
ver.bestHeight = chain == null ? 0 : chain.getBestChainHeight();
ver.time = Utils.now().getTime() / 1000;
Peer peer = new Peer(params, ver, address, chain, memoryPool);
peer.addEventListener(startupListener, Threading.SAME_THREAD);
peer.setMinProtocolVersion(vMinRequiredProtocolVersion);
pendingPeers.add(peer);
channels.openConnection(address, peer);
peer.setSocketTimeout(vConnectTimeoutMillis);
// When the channel has connected and version negotiated successfully, handleNewPeer will end up being called on
// a worker thread.
// Set up the address on the TCPNetworkConnection handler object.
// TODO: This is stupid and racy, get rid of it.
TCPNetworkConnection.NetworkHandler networkHandler =
(TCPNetworkConnection.NetworkHandler) future.getChannel().getPipeline().get("codec");
if (networkHandler != null) {
// This can be null in unit tests or apps that don't use TCP connections.
networkHandler.getOwnerObject().setRemoteAddress(address);
}
if (incrementMaxConnections) {
// We don't use setMaxConnections here as that would trigger a recursive attempt to establish a new
// outbound connection.
@@ -789,15 +710,15 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
lock.unlock();
}
}
return future;
return peer;
}
static public Peer peerFromChannelFuture(ChannelFuture future) {
return peerFromChannel(future.getChannel());
}
static public Peer peerFromChannel(Channel channel) {
return ((PeerHandler)channel.getPipeline().get("peer")).getPeer();
/**
* Sets the timeout between when a connection attempt to a peer begins and when the version message exchange
* completes. This does not apply to currently pending peers.
*/
public void setConnectTimeoutMillis(int connectTimeoutMillis) {
this.vConnectTimeoutMillis = connectTimeoutMillis;
}
/**
@@ -852,11 +773,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
// Give the peer a filter that can be used to probabilistically drop transactions that
// aren't relevant to our wallet. We may still receive some false positives, which is
// OK because it helps improve wallet privacy. Old nodes will just ignore the message.
try {
if (bloomFilter != null) peer.setBloomFilter(bloomFilter);
} catch (IOException e) {
// That was quick...already disconnected
}
if (bloomFilter != null) peer.setBloomFilter(bloomFilter);
// Link the peer to the memory pool so broadcast transactions have their confidence levels updated.
peer.setDownloadData(false);
// TODO: The peer should calculate the fast catchup time from the added wallets here.
@@ -875,7 +792,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
peer.addEventListener(getDataListener, Threading.SAME_THREAD);
// And set up event listeners for clients. This will allow them to find out about new transactions and blocks.
for (ListenerRegistration<PeerEventListener> registration : peerEventListeners) {
peer.addEventListener(registration.listener, registration.executor);
peer.addEventListenerWithoutOnDisconnect(registration.listener, registration.executor);
}
setupPingingForNewPeer(peer);
} finally {
@@ -1080,8 +997,6 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
setDownloadPeer(peer);
// startBlockChainDownload will setDownloadData(true) on itself automatically.
peer.startBlockChainDownload();
} catch (IOException e) {
log.error("failed to start block chain download from " + peer, e);
} finally {
lock.unlock();
}
@@ -1335,6 +1250,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
// zap peers if they upgrade early. If we can't find any peers that have our preferred protocol version or
// better then we'll settle for the highest we found instead.
int highestVersion = 0, preferredVersion = 0;
// If/when PREFERRED_VERSION is not equal to vMinRequiredProtocolVersion, reenable the last test in PeerGroupTest.downloadPeerSelection
final int PREFERRED_VERSION = FilteredBlock.MIN_PROTOCOL_VERSION;
for (Peer peer : candidates) {
highestVersion = Math.max(peer.getPeerVersionMessage().clientVersion, highestVersion);

View File

@@ -0,0 +1,232 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.core;
import java.io.*;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.NotYetConnectedException;
import java.util.concurrent.locks.Lock;
import com.google.bitcoin.networkabstraction.AbstractTimeoutHandler;
import com.google.bitcoin.networkabstraction.MessageWriteTarget;
import com.google.bitcoin.networkabstraction.StreamParser;
import com.google.bitcoin.utils.Threading;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* Handles high-level message (de)serialization for peers, acting as the bridge between the
* {@link com.google.bitcoin.networkabstraction} classes and {@link Peer}.
*/
public abstract class PeerSocketHandler extends AbstractTimeoutHandler implements StreamParser {
private static final Logger log = LoggerFactory.getLogger(PeerSocketHandler.class);
// The IP address to which we are connecting.
@VisibleForTesting
InetSocketAddress remoteIp;
private final BitcoinSerializer serializer;
/** If we close() before we know our writeTarget, set this to true to call writeTarget.closeConnection() right away */
private boolean closePending = false;
// writeTarget will be thread-safe, and may call into PeerGroup, which calls us, so we should call it unlocked
@VisibleForTesting MessageWriteTarget writeTarget = null;
// The ByteBuffers passed to us from the writeTarget are static in size, and usually smaller than some messages we
// will receive. For SPV clients, this should be rare (ie we're mostly dealing with small transactions), but for
// messages which are larger than the read buffer, we have to keep a temporary buffer with its bytes.
private byte[] largeReadBuffer;
private int largeReadBufferPos;
private BitcoinSerializer.BitcoinPacketHeader header;
private Lock lock = Threading.lock("PeerSocketHandler");
public PeerSocketHandler(NetworkParameters params, InetSocketAddress peerAddress) {
serializer = new BitcoinSerializer(checkNotNull(params));
this.remoteIp = checkNotNull(peerAddress);
}
/**
* Sends the given message to the peer. Due to the asynchronousness of network programming, there is no guarantee
* the peer will have received it. Throws NotYetConnectedException if we are not yet connected to the remote peer.
* TODO: Maybe use something other than the unchecked NotYetConnectedException here
*/
public void sendMessage(Message message) throws NotYetConnectedException {
lock.lock();
try {
if (writeTarget == null)
throw new NotYetConnectedException();
} finally {
lock.unlock();
}
// TODO: Some round-tripping could be avoided here
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
serializer.serialize(message, out);
writeTarget.writeBytes(out.toByteArray());
} catch (IOException e) {
exceptionCaught(e);
}
}
/**
* Closes the connection to the peer if one exists, or immediately closes the connection as soon as it opens
*/
public void close() {
lock.lock();
try {
if (writeTarget == null) {
closePending = true;
return;
}
} finally {
lock.unlock();
}
writeTarget.closeConnection();
}
@Override
protected void timeoutOccurred() {
close();
}
/**
* Called every time a message is received from the network
*/
protected abstract void processMessage(Message m) throws Exception;
@Override
public int receiveBytes(ByteBuffer buff) {
checkArgument(buff.position() == 0 &&
buff.capacity() >= BitcoinSerializer.BitcoinPacketHeader.HEADER_LENGTH + 4);
try {
// Repeatedly try to deserialize messages until we hit a BufferUnderflowException
for (int i = 0; true; i++) {
// If we are in the middle of reading a message, try to fill that one first, before we expect another
if (largeReadBuffer != null) {
// This can only happen in the first iteration
checkState(i == 0);
// Read new bytes into the largeReadBuffer
int bytesToGet = Math.min(buff.remaining(), largeReadBuffer.length - largeReadBufferPos);
buff.get(largeReadBuffer, largeReadBufferPos, bytesToGet);
largeReadBufferPos += bytesToGet;
// Check the largeReadBuffer's status
if (largeReadBufferPos == largeReadBuffer.length) {
// ...processing a message if one is available
processMessage(serializer.deserializePayload(header, ByteBuffer.wrap(largeReadBuffer)));
largeReadBuffer = null;
header = null;
} else // ...or just returning if we don't have enough bytes yet
return buff.position();
}
// Now try to deserialize any messages left in buff
Message message;
int preSerializePosition = buff.position();
try {
message = serializer.deserialize(buff);
} catch (BufferUnderflowException e) {
// If we went through the whole buffer without a full message, we need to use the largeReadBuffer
if (i == 0 && buff.limit() == buff.capacity()) {
// ...so reposition the buffer to 0 and read the next message header
buff.position(0);
try {
serializer.seekPastMagicBytes(buff);
header = serializer.deserializeHeader(buff);
// Initialize the largeReadBuffer with the next message's size and fill it with any bytes
// left in buff
largeReadBuffer = new byte[header.size];
largeReadBufferPos = buff.remaining();
buff.get(largeReadBuffer, 0, largeReadBufferPos);
} catch (BufferUnderflowException e1) {
// If we went through a whole buffer's worth of bytes without getting a header, give up
// In cases where the buff is just really small, we could create a second largeReadBuffer
// that we use to deserialize the magic+header, but that is rather complicated when the buff
// should probably be at least that big anyway (for efficiency)
throw new ProtocolException("No magic bytes+header after reading " + buff.capacity() + " bytes");
}
} else {
// Reposition the buffer to its original position, which saves us from skipping messages by
// seeking past part of the magic bytes before all of them are in the buffer
buff.position(preSerializePosition);
}
return buff.position();
}
// Process our freshly deserialized message
processMessage(message);
}
} catch (Exception e) {
exceptionCaught(e);
return -1; // Returning -1 also throws an IllegalStateException upstream and kills the connection
}
}
/**
* Sets the {@link MessageWriteTarget} used to write messages to the peer. This should almost never be called, it is
* called automatically by {@link com.google.bitcoin.networkabstraction.NioClient} or
* {@link com.google.bitcoin.networkabstraction.NioClientManager} once the socket finishes initialization.
*/
@Override
public void setWriteTarget(MessageWriteTarget writeTarget) {
lock.lock();
boolean closeNow = false;
try {
closeNow = closePending;
this.writeTarget = writeTarget;
} finally {
lock.unlock();
}
if (closeNow)
writeTarget.closeConnection();
}
@Override
public int getMaxMessageSize() {
return Message.MAX_SIZE;
}
/**
* @return the IP address and port of peer.
*/
public PeerAddress getAddress() {
return new PeerAddress(remoteIp);
}
/** Catch any exceptions, logging them and then closing the channel. */
private void exceptionCaught(Exception e) {
PeerAddress addr = getAddress();
String s = addr == null ? "?" : addr.toString();
if (e instanceof ConnectException || e instanceof IOException) {
// Short message for network errors
log.info(s + " - " + e.getMessage());
} else {
log.warn(s + " - ", e);
Thread.UncaughtExceptionHandler handler = Threading.uncaughtExceptionHandler;
if (handler != null)
handler.uncaughtException(Thread.currentThread(), e);
}
close();
}
}

View File

@@ -1,259 +0,0 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.core;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferInputStream;
import org.jboss.netty.buffer.ChannelBufferOutputStream;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.*;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.codec.replay.ReplayingDecoder;
import org.jboss.netty.handler.codec.replay.VoidEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.jboss.netty.channel.Channels.write;
// TODO: Remove this class and refactor the way we build Netty pipelines.
/**
* <p>A {@code TCPNetworkConnection} is used for connecting to a Bitcoin node over the standard TCP/IP protocol.<p>
*
* <p>{@link TCPNetworkConnection#getHandler()} is part of a Netty Pipeline, downstream of other pipeline stages.</p>
*
*/
public class TCPNetworkConnection implements NetworkConnection {
private static final Logger log = LoggerFactory.getLogger(TCPNetworkConnection.class);
// The IP address to which we are connecting.
private InetAddress remoteIp;
private final NetworkParameters params;
private VersionMessage versionMessage;
private BitcoinSerializer serializer = null;
private VersionMessage myVersionMessage;
private Channel channel;
private NetworkHandler handler;
// For ping nonces.
private Random random = new Random();
/**
* Construct a network connection with the given params and version. If you use this constructor you need to set
* up the Netty pipelines and infrastructure yourself. If all you have is an IP address and port, use the static
* connectTo method.
*
* @param params Defines which network to connect to and details of the protocol.
* @param ver The VersionMessage to announce to the other side of the connection.
*/
public TCPNetworkConnection(NetworkParameters params, VersionMessage ver) {
this.params = params;
this.myVersionMessage = ver;
this.serializer = new BitcoinSerializer(this.params);
this.handler = new NetworkHandler();
}
// Some members that are used for convenience APIs. If the app only uses PeerGroup then these won't be used.
private static NioClientSocketChannelFactory channelFactory;
private SettableFuture<TCPNetworkConnection> handshakeFuture;
/**
* Returns a future for a TCPNetworkConnection that is connected and version negotiated to the given remote address.
* Behind the scenes this method sets up a thread pool and a Netty pipeline that uses it. The equivalent Netty code
* is quite complex so use this method if you aren't writing a complex app. The future completes once version
* handshaking is done, use .get() on the response to wait for it.
*
* @param params The network parameters to use (production or testnet)
* @param address IP address and port to use
* @param connectTimeoutMsec How long to wait before giving up and setting the future to failure.
* @param peer If not null, this peer will be added to the pipeline.
*/
public static ListenableFuture<TCPNetworkConnection> connectTo(NetworkParameters params, InetSocketAddress address,
int connectTimeoutMsec, @Nullable Peer peer) {
synchronized (TCPNetworkConnection.class) {
if (channelFactory == null) {
ExecutorService bossExecutor = Executors.newCachedThreadPool();
ExecutorService workerExecutor = Executors.newCachedThreadPool();
channelFactory = new NioClientSocketChannelFactory(bossExecutor, workerExecutor);
}
}
// Run the connection in the thread pool and wait for it to complete.
ClientBootstrap clientBootstrap = new ClientBootstrap(channelFactory);
ChannelPipeline pipeline = Channels.pipeline();
final TCPNetworkConnection conn = new TCPNetworkConnection(params, new VersionMessage(params, 0));
conn.handshakeFuture = SettableFuture.create();
conn.setRemoteAddress(address);
pipeline.addLast("codec", conn.getHandler());
if (peer != null) pipeline.addLast("peer", peer.getHandler());
clientBootstrap.setPipeline(pipeline);
clientBootstrap.setOption("connectTimeoutMillis", connectTimeoutMsec);
ChannelFuture socketFuture = clientBootstrap.connect(address);
// Once the socket is either connected on the TCP level, or failed ...
socketFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture channelFuture) throws Exception {
// Check if it failed ...
if (channelFuture.isDone() && !channelFuture.isSuccess()) {
// And complete the returned future with an exception.
conn.handshakeFuture.setException(channelFuture.getCause());
}
// Otherwise the handshakeFuture will be marked as completed once we did ver/verack exchange.
}
});
return conn.handshakeFuture;
}
public void writeMessage(Message message) throws IOException {
write(channel, message);
}
private void onVersionMessage(Message m) throws IOException, ProtocolException {
if (!(m instanceof VersionMessage)) {
// Bad peers might not follow the protocol. This has been seen in the wild (issue 81).
log.info("First message received was not a version message but rather " + m);
return;
}
versionMessage = (VersionMessage) m;
// Switch to the new protocol version.
int peerVersion = versionMessage.clientVersion;
log.info("Connected to {}: version={}, subVer='{}', services=0x{}, time={}, blocks={}",
getPeerAddress().getAddr().getHostAddress(),
peerVersion,
versionMessage.subVer,
versionMessage.localServices,
new Date(versionMessage.time * 1000),
versionMessage.bestHeight);
// Now it's our turn ...
// Send an ACK message stating we accept the peers protocol version.
write(channel, new VersionAck());
// 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. Some bogus
// implementations claim to have a block chain in their services field but then report a height of zero, filter
// them out here.
if (!versionMessage.hasBlockChain() ||
(!params.allowEmptyPeerChain() && versionMessage.bestHeight <= 0)) {
// Shut down the channel
throw new ProtocolException("Peer does not have a copy of the block chain.");
}
// Handshake is done!
if (handshakeFuture != null)
handshakeFuture.set(this);
}
public void ping() throws IOException {
// pong/nonce messages were added to any protocol version greater than 60000
if (versionMessage.clientVersion > 60000) {
write(channel, new Ping(random.nextLong()));
}
else
write(channel, new Ping());
}
@Override
public String toString() {
return "[" + remoteIp.getHostAddress() + "]:" + params.getPort();
}
public class NetworkHandler extends ReplayingDecoder<VoidEnum> implements ChannelDownstreamHandler {
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
super.channelConnected(ctx, e);
channel = e.getChannel();
// The version message does not use checksumming, until Feb 2012 when it magically does.
// 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.
log.info("Announcing to {} as: {}", channel.getRemoteAddress(), myVersionMessage.subVer);
write(channel, myVersionMessage);
// 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.
}
// Attempt to decode a Bitcoin message passing upstream in the channel.
//
// By extending ReplayingDecoder, reading past the end of buffer will throw a special Error
// causing the channel to read more and retry.
//
// On VMs/systems where exception handling is slow, this will impact performance. On the
// other hand, implementing a FrameDecoder will increase code complexity due to having
// to implement retries ourselves.
//
// TODO: consider using a decoder state and checkpoint() if performance is an issue.
@Override
protected Object decode(ChannelHandlerContext ctx, Channel chan,
ChannelBuffer buffer, VoidEnum state) throws Exception {
Message message = serializer.deserialize(new ChannelBufferInputStream(buffer));
if (message instanceof VersionMessage)
onVersionMessage(message);
return message;
}
/** Serialize outgoing Bitcoin messages passing downstream in the channel. */
public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent evt) throws Exception {
if (!(evt instanceof MessageEvent)) {
ctx.sendDownstream(evt);
return;
}
MessageEvent e = (MessageEvent) evt;
Message message = (Message)e.getMessage();
ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
serializer.serialize(message, new ChannelBufferOutputStream(buffer));
write(ctx, e.getFuture(), buffer, e.getRemoteAddress());
}
public TCPNetworkConnection getOwnerObject() {
return TCPNetworkConnection.this;
}
}
/** Returns the Netty Pipeline stage handling Bitcoin serialization for this connection. */
public NetworkHandler getHandler() {
return handler;
}
public VersionMessage getVersionMessage() {
return versionMessage;
}
public PeerAddress getPeerAddress() {
return new PeerAddress(remoteIp, params.getPort());
}
public void close() {
channel.close();
}
public void setRemoteAddress(SocketAddress address) {
if (address instanceof InetSocketAddress)
remoteIp = ((InetSocketAddress)address).getAddress();
}
}

View File

@@ -1774,7 +1774,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
* @throws InsufficientMoneyException if the request could not be completed due to not enough balance.
* @throws IOException if there was a problem broadcasting the transaction
*/
public Transaction sendCoins(Peer peer, SendRequest request) throws IOException, InsufficientMoneyException {
public Transaction sendCoins(Peer peer, SendRequest request) throws InsufficientMoneyException {
Transaction tx = sendCoinsOffline(request);
peer.sendMessage(tx);
return tx;

View File

@@ -14,13 +14,15 @@
* limitations under the License.
*/
package com.google.bitcoin.protocols.niowrapper;
package com.google.bitcoin.networkabstraction;
import java.util.Timer;
import java.util.TimerTask;
/**
* <p>A stream parser that provides functionality for creating timeouts between arbitrary events.</p>
* <p>A base class which provides basic support for socket timeouts. It is used instead of integrating timeouts into the
* NIO select thread both for simplicity and to keep code shared between NIO and blocking sockets as much as possible.
* </p>
*/
public abstract class AbstractTimeoutHandler {
// TimerTask and timeout value which are added to a timer to kill the connection on timeout
@@ -29,7 +31,7 @@ public abstract class AbstractTimeoutHandler {
private boolean timeoutEnabled = true;
// A timer which manages expiring channels as their timeouts occur (if configured).
private static final Timer timeoutTimer = new Timer("ProtobufParser timeouts", true);
private static final Timer timeoutTimer = new Timer("AbstractTimeoutHandler timeouts", true);
/**
* <p>Enables or disables the timeout entirely. This may be useful if you want to store the timeout value but wish

View File

@@ -14,31 +14,37 @@
* limitations under the License.
*/
package com.google.bitcoin.protocols.niowrapper;
package com.google.bitcoin.networkabstraction;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.io.InputStream;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SocketChannel;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkState;
/**
* Creates a simple connection to a server using a {@link StreamParser} to process data.
* <p>Creates a simple connection to a server using a {@link StreamParser} to process data.</p>
*
* <p>Generally, using {@link NioClient} and {@link NioClientManager} should be preferred over {@link BlockingClient}
* and {@link BlockingClientManager}, unless you wish to connect over a proxy or use some other network settings that
* cannot be set using NIO.</p>
*/
public class NioClient implements MessageWriteTarget {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(NioClient.class);
public class BlockingClient implements MessageWriteTarget {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(BlockingClient.class);
private static final int BUFFER_SIZE_LOWER_BOUND = 4096;
private static final int BUFFER_SIZE_UPPER_BOUND = 65536;
@Nonnull private final ByteBuffer dbuf;
@Nonnull private final SocketChannel sc;
@Nonnull private final Socket socket;
private volatile boolean vCloseRequested = false;
/**
* <p>Creates a new client to the given server address using the given {@link StreamParser} to decode the data.
@@ -48,28 +54,35 @@ public class NioClient implements MessageWriteTarget {
*
* @param connectTimeoutMillis The connect timeout set on the connection (in milliseconds). 0 is interpreted as no
* timeout.
* @param clientSet A set which this object will add itself to after initialization, and then remove itself from
* when the connection dies. Note that this set must be thread-safe.
*/
public NioClient(final InetSocketAddress serverAddress, final StreamParser parser,
final int connectTimeoutMillis) throws IOException {
public BlockingClient(final SocketAddress serverAddress, final StreamParser parser,
final int connectTimeoutMillis, @Nullable final Set<BlockingClient> clientSet) throws IOException {
// Try to fit at least one message in the network buffer, but place an upper and lower limit on its size to make
// sure it doesnt get too large or have to call read too often.
dbuf = ByteBuffer.allocateDirect(Math.min(Math.max(parser.getMaxMessageSize(), BUFFER_SIZE_LOWER_BOUND), BUFFER_SIZE_UPPER_BOUND));
parser.setWriteTarget(this);
sc = SocketChannel.open();
socket = new Socket();
new Thread() {
Thread t = new Thread() {
@Override
public void run() {
if (clientSet != null)
clientSet.add(BlockingClient.this);
try {
sc.socket().connect(serverAddress, connectTimeoutMillis);
socket.connect(serverAddress, connectTimeoutMillis);
parser.connectionOpened();
InputStream stream = socket.getInputStream();
byte[] readBuff = new byte[dbuf.capacity()];
while (true) {
int read = sc.read(dbuf);
if (read == 0)
continue;
else if (read == -1)
// TODO Kill the message duplication here
checkState(dbuf.remaining() > 0 && dbuf.remaining() <= readBuff.length);
int read = stream.read(readBuff, 0, Math.max(1, Math.min(dbuf.remaining(), stream.available())));
if (read == -1)
return;
dbuf.put(readBuff, 0, read);
// "flip" the buffer - setting the limit to the current position and setting position to 0
dbuf.flip();
// Use parser.receiveBytes's return value as a double-check that it stopped reading at the right
@@ -80,20 +93,24 @@ public class NioClient implements MessageWriteTarget {
// position)
dbuf.compact();
}
} catch (AsynchronousCloseException e) {// Expected if the connection is closed
} catch (ClosedChannelException e) { // Expected if the connection is closed
} catch (Exception e) {
log.error("Error trying to open/read from connection", e);
if (!vCloseRequested)
log.error("Error trying to open/read from connection", e);
} finally {
try {
sc.close();
socket.close();
} catch (IOException e1) {
// At this point there isn't much we can do, and we can probably assume the channel is closed
}
if (clientSet != null)
clientSet.remove(BlockingClient.this);
parser.connectionClosed();
}
}
}.start();
};
t.setName("BlockingClient network thread for " + serverAddress);
t.setDaemon(true);
t.start();
}
/**
@@ -103,21 +120,21 @@ public class NioClient implements MessageWriteTarget {
public void closeConnection() {
// Closes the channel, triggering an exception in the network-handling thread triggering connectionClosed()
try {
sc.close();
vCloseRequested = true;
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Writes raw bytes to the channel (used by the write method in StreamParser)
@Override
public synchronized void writeBytes(byte[] message) {
public synchronized void writeBytes(byte[] message) throws IOException {
try {
if (sc.write(ByteBuffer.wrap(message)) != message.length)
throw new IOException("Couldn't write all of message to socket");
socket.getOutputStream().write(message);
} catch (IOException e) {
log.error("Error writing message to connection, closing connection", e);
closeConnection();
throw e;
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.networkabstraction;
import com.google.common.util.concurrent.AbstractIdleService;
import java.io.IOException;
import java.net.SocketAddress;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* <p>A thin wrapper around a set of {@link BlockingClient}s.</p>
*
* <p>Generally, using {@link NioClient} and {@link NioClientManager} should be preferred over {@link BlockingClient}
* and {@link BlockingClientManager} as they scale significantly better, unless you wish to connect over a proxy or use
* some other network settings that cannot be set using NIO.</p>
*/
public class BlockingClientManager extends AbstractIdleService implements ClientConnectionManager {
private final Set<BlockingClient> clients = Collections.synchronizedSet(new HashSet<BlockingClient>());
@Override
public void openConnection(SocketAddress serverAddress, StreamParser parser) {
if (!isRunning())
throw new IllegalStateException();
try {
new BlockingClient(serverAddress, parser, 1000, clients);
} catch (IOException e) {
throw new RuntimeException(e); // This should only happen if we are, eg, out of system resources
}
}
@Override
protected void startUp() throws Exception { }
@Override
protected void shutDown() throws Exception {
synchronized (clients) {
for (BlockingClient client : clients)
client.closeConnection();
}
}
@Override
public int getConnectedClientCount() {
return clients.size();
}
@Override
public void closeConnections(int n) {
if (!isRunning())
throw new IllegalStateException();
synchronized (clients) {
Iterator<BlockingClient> it = clients.iterator();
while (n-- > 0 && it.hasNext())
it.next().closeConnection();
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.networkabstraction;
import com.google.common.util.concurrent.Service;
import java.io.IOException;
import java.net.SocketAddress;
/**
* <p>A generic interface for an object which keeps track of a set of open client connections, creates new ones and
* ensures they are serviced properly.</p>
*
* <p>When the service is {@link com.google.common.util.concurrent.Service#stop()}ed, all connections will be closed and
* the appropriate connectionClosed() calls must be made.</p>
*/
public interface ClientConnectionManager extends Service {
/**
* Creates a new connection to the given address, with the given parser used to handle incoming data.
*/
void openConnection(SocketAddress serverAddress, StreamParser parser);
/** Gets the number of connected peers */
int getConnectedClientCount();
/** Closes n peer connections */
void closeConnections(int n);
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.networkabstraction;
import com.google.bitcoin.core.Message;
import com.google.bitcoin.utils.Threading;
import org.slf4j.LoggerFactory;
import javax.annotation.concurrent.GuardedBy;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* A simple NIO MessageWriteTarget which handles all the business logic of a connection (reading+writing bytes).
* Used only by the NioClient and NioServer classes
*/
class ConnectionHandler implements MessageWriteTarget {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(ConnectionHandler.class);
private static final int BUFFER_SIZE_LOWER_BOUND = 4096;
private static final int BUFFER_SIZE_UPPER_BOUND = 65536;
private static final int OUTBOUND_BUFFER_BYTE_COUNT = Message.MAX_SIZE + 24; // 24 byte message header
// We lock when touching local flags and when writing data, but NEVER when calling any methods which leave this
// class into non-Java classes.
private final ReentrantLock lock = Threading.lock("nioConnectionHandler");
@GuardedBy("lock") private final ByteBuffer readBuff;
@GuardedBy("lock") private final SocketChannel channel;
@GuardedBy("lock") private final SelectionKey key;
@GuardedBy("lock") final StreamParser parser;
@GuardedBy("lock") private boolean closeCalled = false;
@GuardedBy("lock") private long bytesToWriteRemaining = 0;
@GuardedBy("lock") private final LinkedList<ByteBuffer> bytesToWrite = new LinkedList<ByteBuffer>();
private Set<ConnectionHandler> connectedHandlers;
public ConnectionHandler(StreamParserFactory parserFactory, SelectionKey key) throws IOException {
this(parserFactory.getNewParser(((SocketChannel)key.channel()).socket().getInetAddress(), ((SocketChannel)key.channel()).socket().getPort()), key);
if (parser == null)
throw new IOException("Parser factory.getNewParser returned null");
}
private ConnectionHandler(StreamParser parser, SelectionKey key) {
this.key = key;
this.channel = checkNotNull(((SocketChannel)key.channel()));
this.parser = parser;
if (parser == null) {
readBuff = null;
closeConnection();
return;
}
readBuff = ByteBuffer.allocateDirect(Math.min(Math.max(parser.getMaxMessageSize(), BUFFER_SIZE_LOWER_BOUND), BUFFER_SIZE_UPPER_BOUND));
parser.setWriteTarget(this); // May callback into us (eg closeConnection() now)
connectedHandlers = null;
}
public ConnectionHandler(StreamParser parser, SelectionKey key, Set<ConnectionHandler> connectedHandlers) {
this(checkNotNull(parser), key);
// closeConnection() may have already happened, in which case we shouldn't add ourselves to the connectedHandlers set
lock.lock();
boolean alreadyClosed = false;
try {
alreadyClosed = closeCalled;
this.connectedHandlers = connectedHandlers;
} finally {
lock.unlock();
}
if (!alreadyClosed)
checkState(connectedHandlers.add(this));
}
// Tries to write any outstanding write bytes, runs in any thread (possibly unlocked)
private void tryWriteBytes() throws IOException {
lock.lock();
try {
// Iterate through the outbound ByteBuff queue, pushing as much as possible into the OS' network buffer.
Iterator<ByteBuffer> bytesIterator = bytesToWrite.iterator();
while (bytesIterator.hasNext()) {
ByteBuffer buff = bytesIterator.next();
bytesToWriteRemaining -= channel.write(buff);
if (!buff.hasRemaining())
bytesIterator.remove();
else {
// Make sure we are registered to get updated when writing is available again
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
// Refresh the selector to make sure it gets the new interestOps
key.selector().wakeup();
break;
}
}
// If we are done writing, clear the OP_WRITE interestOps
if (bytesToWrite.isEmpty())
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
// Don't bother waking up the selector here, since we're just removing an op, not adding
} finally {
lock.unlock();
}
}
@Override
public void writeBytes(byte[] message) throws IOException {
lock.lock();
try {
// Network buffers are not unlimited (and are often smaller than some messages we may wish to send), and
// thus we have to buffer outbound messages sometimes. To do this, we use a queue of ByteBuffers and just
// append to it when we want to send a message. We then let tryWriteBytes() either send the message or
// register our SelectionKey to wakeup when we have free outbound buffer space available.
if (bytesToWriteRemaining + message.length > OUTBOUND_BUFFER_BYTE_COUNT)
throw new IOException("Outbound buffer overflowed");
// Just dump the message onto the write buffer and call tryWriteBytes
// TODO: Kill the needless message duplication when the write completes right away
bytesToWrite.offer(ByteBuffer.wrap(Arrays.copyOf(message, message.length)));
bytesToWriteRemaining += message.length;
tryWriteBytes();
} catch (IOException e) {
lock.unlock();
log.error("Error writing message to connection, closing connection", e);
closeConnection();
throw e;
}
lock.unlock();
}
@Override
// May NOT be called with lock held
public void closeConnection() {
try {
channel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
connectionClosed();
}
private void connectionClosed() {
boolean callClosed = false;
lock.lock();
try {
callClosed = !closeCalled;
closeCalled = true;
} finally {
lock.unlock();
}
if (callClosed) {
checkState(connectedHandlers == null || connectedHandlers.remove(this));
parser.connectionClosed();
}
}
// Handle a SelectionKey which was selected
// Runs unlocked as the caller is single-threaded (or if not, should enforce that handleKey is only called
// atomically for a given ConnectionHandler)
public static void handleKey(SelectionKey key) {
ConnectionHandler handler = ((ConnectionHandler)key.attachment());
try {
if (handler == null)
return;
if (!key.isValid()) {
handler.closeConnection(); // Key has been cancelled, make sure the socket gets closed
return;
}
if (key.isReadable()) {
// Do a socket read and invoke the parser's receiveBytes message
int read = handler.channel.read(handler.readBuff);
if (read == 0)
return; // Was probably waiting on a write
else if (read == -1) { // Socket was closed
key.cancel();
handler.closeConnection();
return;
}
// "flip" the buffer - setting the limit to the current position and setting position to 0
handler.readBuff.flip();
// Use parser.receiveBytes's return value as a check that it stopped reading at the right location
int bytesConsumed = handler.parser.receiveBytes(handler.readBuff);
checkState(handler.readBuff.position() == bytesConsumed);
// Now drop the bytes which were read by compacting readBuff (resetting limit and keeping relative
// position)
handler.readBuff.compact();
}
if (key.isWritable())
handler.tryWriteBytes();
} catch (Exception e) {
// This can happen eg if the channel closes while the thread is about to get killed
// (ClosedByInterruptException), or if handler.parser.receiveBytes throws something
log.error("Error handling SelectionKey", e);
if (handler != null)
handler.closeConnection();
}
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.google.bitcoin.protocols.niowrapper;
package com.google.bitcoin.networkabstraction;
import java.io.IOException;
@@ -22,6 +22,13 @@ import java.io.IOException;
* A target to which messages can be written/connection can be closed
*/
public interface MessageWriteTarget {
/**
* Writes the given bytes to the remote server.
*/
void writeBytes(byte[] message) throws IOException;
/**
* Closes the connection to the server, triggering the {@link StreamParser#connectionClosed()}
* event on the network-handling thread where all callbacks occur.
*/
void closeConnection();
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.networkabstraction;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import javax.annotation.Nonnull;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkState;
/**
* Creates a simple connection to a server using a {@link StreamParser} to process data.
*/
public class NioClient implements MessageWriteTarget {
private final Handler handler;
private final NioClientManager manager = new NioClientManager();
class Handler extends AbstractTimeoutHandler implements StreamParser {
private final StreamParser upstreamParser;
private MessageWriteTarget writeTarget;
private boolean closeOnOpen = false;
Handler(StreamParser upstreamParser, int connectTimeoutMillis) {
this.upstreamParser = upstreamParser;
setSocketTimeout(connectTimeoutMillis);
setTimeoutEnabled(true);
}
@Override
protected synchronized void timeoutOccurred() {
upstreamParser.connectionClosed();
closeOnOpen = true;
}
@Override
public void connectionClosed() {
upstreamParser.connectionClosed();
manager.stop();
}
@Override
public synchronized void connectionOpened() {
if (!closeOnOpen)
upstreamParser.connectionOpened();
}
@Override
public int receiveBytes(ByteBuffer buff) throws Exception {
return upstreamParser.receiveBytes(buff);
}
@Override
public synchronized void setWriteTarget(MessageWriteTarget writeTarget) {
if (closeOnOpen)
writeTarget.closeConnection();
else {
setTimeoutEnabled(false);
this.writeTarget = writeTarget;
upstreamParser.setWriteTarget(writeTarget);
}
}
@Override
public int getMaxMessageSize() {
return upstreamParser.getMaxMessageSize();
}
}
/**
* <p>Creates a new client to the given server address using the given {@link StreamParser} to decode the data.
* The given parser <b>MUST</b> be unique to this object. This does not block while waiting for the connection to
* open, but will call either the {@link StreamParser#connectionOpened()} or
* {@link StreamParser#connectionClosed()} callback on the created network event processing thread.</p>
*
* @param connectTimeoutMillis The connect timeout set on the connection (in milliseconds). 0 is interpreted as no
* timeout.
*/
public NioClient(final SocketAddress serverAddress, final StreamParser parser,
final int connectTimeoutMillis) throws IOException {
manager.startAndWait();
handler = new Handler(parser, connectTimeoutMillis);
manager.openConnection(serverAddress, handler);
}
@Override
public void closeConnection() {
handler.writeTarget.closeConnection();
}
@Override
public synchronized void writeBytes(byte[] message) throws IOException {
handler.writeTarget.writeBytes(message);
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.networkabstraction;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.*;
import java.nio.channels.spi.SelectorProvider;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.AbstractExecutionThreadService;
import org.slf4j.LoggerFactory;
/**
* A class which manages a set of client connections. Uses Java NIO to select network events and processes them in a
* single network processing thread.
*/
public class NioClientManager extends AbstractExecutionThreadService implements ClientConnectionManager {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(NioClientManager.class);
private final Selector selector;
// SocketChannels and StreamParsers of newly-created connections which should be registered with OP_CONNECT
class SocketChannelAndParser {
SocketChannel sc; StreamParser parser;
SocketChannelAndParser(SocketChannel sc, StreamParser parser) { this.sc = sc; this.parser = parser; }
}
final Queue<SocketChannelAndParser> newConnectionChannels = new LinkedBlockingQueue<SocketChannelAndParser>();
// Added to/removed from by the individual ConnectionHandler's, thus must by synchronized on its own.
private final Set<ConnectionHandler> connectedHandlers = Collections.synchronizedSet(new HashSet<ConnectionHandler>());
// Handle a SelectionKey which was selected
private void handleKey(SelectionKey key) throws IOException {
// We could have a !isValid() key here if the connection is already closed at this point
if (key.isValid() && key.isConnectable()) { // ie a client connection which has finished the initial connect process
// Create a ConnectionHandler and hook everything together
StreamParser parser = (StreamParser) key.attachment();
SocketChannel sc = (SocketChannel) key.channel();
ConnectionHandler handler = new ConnectionHandler(parser, key, connectedHandlers);
try {
if (sc.finishConnect()) {
log.info("Successfully connected to {}", sc.socket().getRemoteSocketAddress());
handler.parser.connectionOpened();
key.interestOps(SelectionKey.OP_READ).attach(handler);
} else {
log.error("Failed to connect to {}", sc.socket().getRemoteSocketAddress());
handler.closeConnection(); // Failed to connect for some reason
}
} catch (IOException e) {
// Calling sc.socket().getRemoteSocketAddress() here throws an exception, so we can only log the error itself
log.error("Failed to connect with exception", e);
handler.closeConnection();
} catch (CancelledKeyException e) { // There is a race to get to interestOps after finishConnect() which may cause this
// Calling sc.socket().getRemoteSocketAddress() here throws an exception, so we can only log the error itself
log.error("Failed to connect with exception", e);
handler.closeConnection();
}
} else // Process bytes read
ConnectionHandler.handleKey(key);
}
/**
* Creates a new client manager which uses Java NIO for socket management. Uses a single thread to handle all select
* calls.
*/
public NioClientManager() {
try {
selector = SelectorProvider.provider().openSelector();
} catch (IOException e) {
throw new RuntimeException(e); // Shouldn't ever happen
}
}
@Override
public void run() {
try {
while (isRunning()) {
SocketChannelAndParser conn;
while ((conn = newConnectionChannels.poll()) != null) {
SelectionKey key = null;
try {
key = conn.sc.register(selector, SelectionKey.OP_CONNECT);
} catch (ClosedChannelException e) {
log.info("SocketChannel was closed before it could be registered");
}
key.attach(conn.parser);
}
selector.select();
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
handleKey(key);
}
}
} catch (Exception e) {
log.error("Error trying to open/read from connection: ", e);
} finally {
// Go through and close everything, without letting IOExceptions get in our way
for (SelectionKey key : selector.keys()) {
try {
key.channel().close();
} catch (IOException e) {
log.error("Error closing channel", e);
}
key.cancel();
if (key.attachment() instanceof ConnectionHandler)
ConnectionHandler.handleKey(key); // Close connection if relevant
}
try {
selector.close();
} catch (IOException e) {
log.error("Error closing client manager selector", e);
}
}
}
@Override
public void openConnection(SocketAddress serverAddress, StreamParser parser) {
if (!isRunning())
throw new IllegalStateException();
// Create a new connection, give it a parser as an attachment
try {
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(serverAddress);
newConnectionChannels.offer(new SocketChannelAndParser(sc, parser));
selector.wakeup();
} catch (IOException e) {
throw new RuntimeException(e); // This should only happen if we are, eg, out of system resources
}
}
@Override
public void triggerShutdown() {
selector.wakeup();
}
@Override
public int getConnectedClientCount() {
return connectedHandlers.size();
}
@Override
public void closeConnections(int n) {
while (n-- > 0) {
ConnectionHandler handler;
synchronized (connectedHandlers) {
handler = connectedHandlers.iterator().next();
}
if (handler != null)
handler.closeConnection(); // Removes handler from connectedHandlers before returning
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.networkabstraction;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.*;
import java.nio.channels.spi.SelectorProvider;
import java.util.Iterator;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.AbstractExecutionThreadService;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* Creates a simple server listener which listens for incoming client connections and uses a {@link StreamParser} to
* process data.
*/
public class NioServer extends AbstractExecutionThreadService {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(NioServer.class);
private final StreamParserFactory parserFactory;
private final ServerSocketChannel sc;
@VisibleForTesting final Selector selector;
// Handle a SelectionKey which was selected
private void handleKey(Selector selector, SelectionKey key) throws IOException {
if (key.isValid() && key.isAcceptable()) {
// Accept a new connection, give it a parser as an attachment
SocketChannel newChannel = sc.accept();
newChannel.configureBlocking(false);
SelectionKey newKey = newChannel.register(selector, SelectionKey.OP_READ);
ConnectionHandler handler = new ConnectionHandler(parserFactory, newKey);
newKey.attach(handler);
handler.parser.connectionOpened();
} else { // Got a closing channel or a channel to a client connection
ConnectionHandler.handleKey(key);
}
}
/**
* Creates a new server which is capable of listening for incoming connections and processing client provided data
* using {@link StreamParser}s created by the given {@link StreamParserFactory}
*
* @throws IOException If there is an issue opening the server socket or binding fails for some reason
*/
public NioServer(final StreamParserFactory parserFactory, InetSocketAddress bindAddress) throws IOException {
this.parserFactory = parserFactory;
sc = ServerSocketChannel.open();
sc.configureBlocking(false);
sc.socket().bind(bindAddress);
selector = SelectorProvider.provider().openSelector();
sc.register(selector, SelectionKey.OP_ACCEPT);
}
@Override
protected void run() throws Exception {
try {
while (isRunning()) {
selector.select();
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
handleKey(selector, key);
}
}
} catch (Exception e) {
log.error("Error trying to open/read from connection: {}", e);
} finally {
// Go through and close everything, without letting IOExceptions get in our way
for (SelectionKey key : selector.keys()) {
try {
key.channel().close();
} catch (IOException e) {
log.error("Error closing channel", e);
}
try {
key.cancel();
handleKey(selector, key);
} catch (IOException e) {
log.error("Error closing selection key", e);
}
}
try {
selector.close();
} catch (IOException e) {
log.error("Error closing server selector", e);
}
try {
sc.close();
} catch (IOException e) {
log.error("Error closing server channel", e);
}
}
}
/**
* Invoked by the Execution service when it's time to stop.
* Calling this method directly will NOT stop the service, call
* {@link com.google.common.util.concurrent.AbstractExecutionThreadService#stop()} instead.
*/
@Override
public void triggerShutdown() {
// Wake up the selector and let the selection thread break its loop as the ExecutionService !isRunning()
selector.wakeup();
}
}

View File

@@ -14,10 +14,11 @@
* limitations under the License.
*/
package com.google.bitcoin.protocols.niowrapper;
package com.google.bitcoin.networkabstraction;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.utils.Threading;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.ByteString;
import com.google.protobuf.MessageLite;
import org.slf4j.Logger;
@@ -74,7 +75,7 @@ public class ProtobufParser<MessageType extends MessageLite> extends AbstractTim
@GuardedBy("lock") private byte[] messageBytes;
private final ReentrantLock lock = Threading.lock("ProtobufParser");
private final AtomicReference<MessageWriteTarget> writeTarget = new AtomicReference<MessageWriteTarget>();
@VisibleForTesting final AtomicReference<MessageWriteTarget> writeTarget = new AtomicReference<MessageWriteTarget>();
/**
* Creates a new protobuf handler.

View File

@@ -14,12 +14,13 @@
* limitations under the License.
*/
package com.google.bitcoin.protocols.niowrapper;
package com.google.bitcoin.networkabstraction;
import java.nio.ByteBuffer;
/**
* A generic handler which is used in {@link NioServer} and {@link NioClient} to handle incoming data streams.
* A generic handler which is used in {@link NioServer}, {@link NioClient} and {@link BlockingClient} to handle incoming
* data streams.
*/
public interface StreamParser {
/** Called when the connection socket is closed */
@@ -29,14 +30,22 @@ public interface StreamParser {
void connectionOpened();
/**
* Called when new bytes are available from the remote end.
* * buff will start with its limit set to the position we can read to and its position set to the location we will
* start reading at
* * May read more than one message (recursively) if there are enough bytes available
* * Uses messageBytes/messageBytesOffset to store message which are larger (incl their length prefix) than buff's
* capacity(), ie it is up to this method to ensure we dont run out of buffer space to decode the next message.
* * buff will end with its limit the same as it was previously, and its position set to the position up to which
* bytes have been read (the same as its return value)
* <p>Called when new bytes are available from the remote end. This should only ever be called by the single
* writeTarget associated with any given StreamParser, multiple callers will likely confuse implementations.</p>
*
* Implementers/callers must follow the following conventions exactly:
* <ul>
* <li>buff will start with its limit set to the position we can read to and its position set to the location we
* will start reading at (always 0)</li>
* <li>May read more than one message (recursively) if there are enough bytes available</li>
* <li>Uses some internal buffering to store message which are larger (incl their length prefix) than buff's
* capacity(), ie it is up to this method to ensure we dont run out of buffer space to decode the next message.
* </li>
* <li>buff will end with its limit the same as it was previously, and its position set to the position up to which
* bytes have been read (the same as its return value)</li>
* <li>buff must be at least the size of a Bitcoin header (incl magic bytes).</li>
* </ul>
*
* @return The amount of bytes consumed which should not be provided again
*/
int receiveBytes(ByteBuffer buff) throws Exception;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.google.bitcoin.protocols.niowrapper;
package com.google.bitcoin.networkabstraction;
import java.net.InetAddress;
import javax.annotation.Nullable;

View File

@@ -20,8 +20,8 @@ import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.InsufficientMoneyException;
import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.protocols.niowrapper.NioClient;
import com.google.bitcoin.protocols.niowrapper.ProtobufParser;
import com.google.bitcoin.networkabstraction.NioClient;
import com.google.bitcoin.networkabstraction.ProtobufParser;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.bitcoin.paymentchannel.Protos;

View File

@@ -27,9 +27,9 @@ import javax.annotation.Nullable;
import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.core.TransactionBroadcaster;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.protocols.niowrapper.NioServer;
import com.google.bitcoin.protocols.niowrapper.ProtobufParser;
import com.google.bitcoin.protocols.niowrapper.StreamParserFactory;
import com.google.bitcoin.networkabstraction.NioServer;
import com.google.bitcoin.networkabstraction.ProtobufParser;
import com.google.bitcoin.networkabstraction.StreamParserFactory;
import org.bitcoin.paymentchannel.Protos;
import static com.google.common.base.Preconditions.checkNotNull;
@@ -48,7 +48,8 @@ public class PaymentChannelServerListener {
private final HandlerFactory eventHandlerFactory;
private final BigInteger minAcceptedChannelSize;
private final NioServer server;
private NioServer server;
private final int timeoutSeconds;
/**
* A factory which generates connection-specific event handlers.
@@ -136,7 +137,13 @@ public class PaymentChannelServerListener {
* @throws Exception If binding to the given port fails (eg SocketException: Permission denied for privileged ports)
*/
public void bindAndStart(int port) throws Exception {
server.start(new InetSocketAddress(port));
server = new NioServer(new StreamParserFactory() {
@Override
public ProtobufParser getNewParser(InetAddress inetAddress, int port) {
return new ServerHandler(new InetSocketAddress(inetAddress, port), timeoutSeconds).socketProtobufHandler;
}
}, new InetSocketAddress(port));
server.startAndWait();
}
/**
@@ -159,13 +166,7 @@ public class PaymentChannelServerListener {
this.broadcaster = checkNotNull(broadcaster);
this.eventHandlerFactory = checkNotNull(eventHandlerFactory);
this.minAcceptedChannelSize = checkNotNull(minAcceptedChannelSize);
server = new NioServer(new StreamParserFactory() {
@Override
public ProtobufParser getNewParser(InetAddress inetAddress, int port) {
return new ServerHandler(new InetSocketAddress(inetAddress, port), timeoutSeconds).socketProtobufHandler;
}
});
this.timeoutSeconds = timeoutSeconds;
}
/**
@@ -176,10 +177,6 @@ public class PaymentChannelServerListener {
* wallet.</p>
*/
public void close() {
try {
server.stop();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
server.stopAndWait();
}
}

View File

@@ -19,7 +19,7 @@ package com.google.bitcoin.protocols.channels;
import java.math.BigInteger;
import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.protocols.niowrapper.ProtobufParser;
import com.google.bitcoin.networkabstraction.ProtobufParser;
import org.bitcoin.paymentchannel.Protos;
/**

View File

@@ -1,130 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.protocols.niowrapper;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.locks.ReentrantLock;
import com.google.bitcoin.utils.Threading;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* A simple connection handler which handles all the business logic of a connection
*/
class ConnectionHandler implements MessageWriteTarget {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(ConnectionHandler.class);
private static final int BUFFER_SIZE_LOWER_BOUND = 4096;
private static final int BUFFER_SIZE_UPPER_BOUND = 65536;
private final ReentrantLock lock = Threading.lock("nioConnectionHandler");
private final ByteBuffer dbuf;
private final SocketChannel channel;
final StreamParser parser;
private boolean closeCalled = false;
ConnectionHandler(StreamParserFactory parserFactory, SocketChannel channel) throws IOException {
this.channel = checkNotNull(channel);
StreamParser newParser = parserFactory.getNewParser(channel.socket().getInetAddress(), channel.socket().getPort());
if (newParser == null) {
closeConnection();
throw new IOException("Parser factory.getNewParser returned null");
}
this.parser = newParser;
dbuf = ByteBuffer.allocateDirect(Math.min(Math.max(parser.getMaxMessageSize(), BUFFER_SIZE_LOWER_BOUND), BUFFER_SIZE_UPPER_BOUND));
newParser.setWriteTarget(this);
}
@Override
public void writeBytes(byte[] message) throws IOException {
lock.lock();
try {
if (channel.write(ByteBuffer.wrap(message)) != message.length)
throw new IOException("Couldn't write all of message to socket");
} catch (IOException e) {
log.error("Error writing message to connection, closing connection", e);
closeConnection();
throw e;
} finally {
lock.unlock();
}
}
@Override
public void closeConnection() {
try {
channel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
connectionClosed();
}
private void connectionClosed() {
boolean callClosed = false;
lock.lock();
try {
callClosed = !closeCalled;
closeCalled = true;
} finally {
lock.unlock();
}
if (callClosed)
parser.connectionClosed();
}
// Handle a SelectionKey which was selected
static void handleKey(SelectionKey key) throws IOException {
ConnectionHandler handler = ((ConnectionHandler)key.attachment());
try {
if (!key.isValid() && handler != null)
handler.closeConnection(); // Key has been cancelled, make sure the socket gets closed
else if (handler != null && key.isReadable()) {
// Do a socket read and invoke the parser's receiveBytes message
int read = handler.channel.read(handler.dbuf);
if (read == 0)
return; // Should probably never happen, but just in case it actually can just return 0
else if (read == -1) { // Socket was closed
key.cancel();
handler.closeConnection();
return;
}
// "flip" the buffer - setting the limit to the current position and setting position to 0
handler.dbuf.flip();
// Use parser.receiveBytes's return value as a check that it stopped reading at the right location
int bytesConsumed = handler.parser.receiveBytes(handler.dbuf);
checkState(handler.dbuf.position() == bytesConsumed);
// Now drop the bytes which were read by compacting dbuf (resetting limit and keeping relative
// position)
handler.dbuf.compact();
}
} catch (Exception e) {
// This can happen eg if the channel closes while the tread is about to get killed
// (ClosedByInterruptException), or if parser.parser.receiveBytes throws something
log.error("Error handling SelectionKey", e);
if (handler != null)
handler.closeConnection();
}
}
}

View File

@@ -1,138 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.protocols.niowrapper;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Iterator;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* Creates a simple server listener which listens for incoming client connections and uses a {@link StreamParser} to
* process data.
*/
public class NioServer {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(NioServer.class);
private final StreamParserFactory parserFactory;
@VisibleForTesting final Thread handlerThread;
private final ServerSocketChannel sc;
// Handle a SelectionKey which was selected
private void handleKey(Selector selector, SelectionKey key) throws IOException {
if (key.isValid() && key.isAcceptable()) {
// Accept a new connection, give it a parser as an attachment
SocketChannel newChannel = sc.accept();
newChannel.configureBlocking(false);
ConnectionHandler handler = new ConnectionHandler(parserFactory, newChannel);
newChannel.register(selector, SelectionKey.OP_READ).attach(handler);
handler.parser.connectionOpened();
} else { // Got a closing channel or a channel to a client connection
ConnectionHandler.handleKey(key);
}
}
/**
* Creates a new server which is capable of listening for incoming connections and processing client provided data
* using {@link StreamParser}s created by the given {@link StreamParserFactory}
*
* @throws IOException If there is an issue opening the server socket (note that we don't bind yet)
*/
public NioServer(final StreamParserFactory parserFactory) throws IOException {
this.parserFactory = parserFactory;
sc = ServerSocketChannel.open();
sc.configureBlocking(false);
final Selector selector = SelectorProvider.provider().openSelector();
handlerThread = new Thread() {
@Override
public void run() {
try {
sc.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) { // Will get 0 on stop() due to thread interrupt
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
handleKey(selector, key);
}
}
} catch (Exception e) {
log.error("Error trying to open/read from connection: {}", e);
} finally {
// Go through and close everything, without letting IOExceptions get in our way
for (SelectionKey key : selector.keys()) {
try {
key.channel().close();
} catch (IOException e) {
log.error("Error closing channel", e);
}
try {
key.cancel();
handleKey(selector, key);
} catch (IOException e) {
log.error("Error closing selection key", e);
}
}
try {
selector.close();
} catch (IOException e) {
log.error("Error closing server selector", e);
}
try {
sc.close();
} catch (IOException e) {
log.error("Error closing server channel", e);
}
}
}
};
}
/**
* Starts the server by binding to the given address and starting the connection handling thread.
*
* @throws IOException If binding fails for some reason.
*/
public void start(InetSocketAddress bindAddress) throws IOException {
sc.socket().bind(bindAddress);
handlerThread.start();
}
/**
* Attempts to gracefully close all open connections, calling their connectionClosed() events.
* @throws InterruptedException If we are interrupted while waiting for the process to finish
*/
public void stop() throws InterruptedException {
handlerThread.interrupt();
handlerThread.join();
}
}

View File

@@ -31,7 +31,7 @@ public class ListenerRegistration<T> {
this.executor = executor;
}
public static <T> boolean removeFromList(T listener, List<ListenerRegistration<T>> list) {
public static <T> boolean removeFromList(T listener, List<? extends ListenerRegistration<T>> list) {
ListenerRegistration<T> item = null;
for (ListenerRegistration<T> registration : list) {
if (registration.listener == listener) {

View File

@@ -24,6 +24,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
public class TestUtils {
public static Transaction createFakeTxWithChangeAddress(NetworkParameters params, BigInteger nanocoins, Address to, Address changeOutput)
@@ -107,7 +108,7 @@ public class TestUtils {
BitcoinSerializer bs = new BitcoinSerializer(params);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bs.serialize(tx, bos);
return (Transaction) bs.deserialize(new ByteArrayInputStream(bos.toByteArray()));
return (Transaction) bs.deserialize(ByteBuffer.wrap(bos.toByteArray()));
}
public static class DoubleSpends {

View File

@@ -21,10 +21,9 @@ import com.google.bitcoin.params.MainNetParams;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static org.junit.Assert.*;
@@ -57,15 +56,14 @@ public class BitcoinSerializerTest {
public void testAddr() throws Exception {
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
// the actual data from https://en.bitcoin.it/wiki/Protocol_specification#addr
ByteArrayInputStream bais = new ByteArrayInputStream(addrMessage);
AddressMessage a = (AddressMessage)bs.deserialize(bais);
AddressMessage a = (AddressMessage)bs.deserialize(ByteBuffer.wrap(addrMessage));
assertEquals(1, a.getAddresses().size());
PeerAddress pa = a.getAddresses().get(0);
assertEquals(8333, pa.getPort());
assertEquals("10.0.0.1", pa.getAddr().getHostAddress());
ByteArrayOutputStream bos = new ByteArrayOutputStream(addrMessage.length);
bs.serialize(a, bos);
//this wont be true due to dynamic timestamps.
//assertTrue(LazyParseByteCacheTest.arrayContains(bos.toByteArray(), addrMessage));
}
@@ -73,86 +71,81 @@ public class BitcoinSerializerTest {
@Test
public void testLazyParsing() throws Exception {
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get(), true, false);
ByteArrayInputStream bais = new ByteArrayInputStream(txMessage);
Transaction tx = (Transaction)bs.deserialize(bais);
Transaction tx = (Transaction)bs.deserialize(ByteBuffer.wrap(txMessage));
assertNotNull(tx);
assertEquals(false, tx.isParsed());
assertEquals(true, tx.isCached());
tx.getInputs();
assertEquals(true, tx.isParsed());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bs.serialize(tx, bos);
assertEquals(true, Arrays.equals(txMessage, bos.toByteArray()));
}
@Test
@Test
public void testCachedParsing() throws Exception {
testCachedParsing(true);
testCachedParsing(false);
}
private void testCachedParsing(boolean lazy) throws Exception {
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get(), lazy, true);
//first try writing to a fields to ensure uncaching and children are not affected
ByteArrayInputStream bais = new ByteArrayInputStream(txMessage);
Transaction tx = (Transaction)bs.deserialize(bais);
Transaction tx = (Transaction)bs.deserialize(ByteBuffer.wrap(txMessage));
assertNotNull(tx);
assertEquals(!lazy, tx.isParsed());
assertEquals(true, tx.isCached());
tx.setLockTime(1);
//parent should have been uncached
assertEquals(false, tx.isCached());
//child should remain cached.
assertEquals(true, tx.getInputs().get(0).isCached());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bs.serialize(tx, bos);
assertEquals(true, !Arrays.equals(txMessage, bos.toByteArray()));
//now try writing to a child to ensure uncaching is propagated up to parent but not to siblings
bais = new ByteArrayInputStream(txMessage);
tx = (Transaction)bs.deserialize(bais);
tx = (Transaction)bs.deserialize(ByteBuffer.wrap(txMessage));
assertNotNull(tx);
assertEquals(!lazy, tx.isParsed());
assertEquals(true, tx.isCached());
tx.getInputs().get(0).setSequenceNumber(1);
//parent should have been uncached
assertEquals(false, tx.isCached());
//so should child
assertEquals(false, tx.getInputs().get(0).isCached());
bos = new ByteArrayOutputStream();
bs.serialize(tx, bos);
assertEquals(true, !Arrays.equals(txMessage, bos.toByteArray()));
//deserialize/reserialize to check for equals.
bais = new ByteArrayInputStream(txMessage);
tx = (Transaction)bs.deserialize(bais);
tx = (Transaction)bs.deserialize(ByteBuffer.wrap(txMessage));
assertNotNull(tx);
assertEquals(!lazy, tx.isParsed());
assertEquals(true, tx.isCached());
bos = new ByteArrayOutputStream();
bs.serialize(tx, bos);
assertEquals(true, Arrays.equals(txMessage, bos.toByteArray()));
//deserialize/reserialize to check for equals. Set a field to it's existing value to trigger uncache
bais = new ByteArrayInputStream(txMessage);
tx = (Transaction)bs.deserialize(bais);
tx = (Transaction)bs.deserialize(ByteBuffer.wrap(txMessage));
assertNotNull(tx);
assertEquals(!lazy, tx.isParsed());
assertEquals(true, tx.isCached());
tx.getInputs().get(0).setSequenceNumber(tx.getInputs().get(0).getSequenceNumber());
bos = new ByteArrayOutputStream();
bs.serialize(tx, bos);
assertEquals(true, Arrays.equals(txMessage, bos.toByteArray()));
}
@@ -163,12 +156,10 @@ public class BitcoinSerializerTest {
public void testHeaders1() throws Exception {
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
ByteArrayInputStream bais = new ByteArrayInputStream(Hex.decode("f9beb4d9686561" +
HeadersMessage hm = (HeadersMessage) bs.deserialize(ByteBuffer.wrap(Hex.decode("f9beb4d9686561" +
"646572730000000000520000005d4fab8101010000006fe28c0ab6f1b372c1a6a246ae6" +
"3f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677b" +
"a1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e3629900"));
HeadersMessage hm = (HeadersMessage) bs.deserialize(bais);
"a1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e3629900")));
// The first block after the genesis
// http://blockexplorer.com/b/1
@@ -190,7 +181,7 @@ public class BitcoinSerializerTest {
public void testHeaders2() throws Exception {
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
ByteArrayInputStream bais = new ByteArrayInputStream(Hex.decode("f9beb4d96865616465" +
HeadersMessage hm = (HeadersMessage) bs.deserialize(ByteBuffer.wrap(Hex.decode("f9beb4d96865616465" +
"72730000000000e701000085acd4ea06010000006fe28c0ab6f1b372c1a6a246ae63f74f931e" +
"8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1c" +
"db606e857233e0e61bc6649ffff001d01e3629900010000004860eb18bf1b1620e37e9490fc8a" +
@@ -203,9 +194,7 @@ public class BitcoinSerializerTest {
"a88d221c8bd6c059da090e88f8a2c99690ee55dbba4e00000000e11c48fecdd9e72510ca84f023" +
"370c9a38bf91ac5cae88019bee94d24528526344c36649ffff001d1d03e4770001000000fc33f5" +
"96f822a0a1951ffdbf2a897b095636ad871707bf5d3162729b00000000379dfb96a5ea8c81700ea4" +
"ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c9700"));
HeadersMessage hm = (HeadersMessage) bs.deserialize(bais);
"ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c9700")));
int nBlocks = hm.getBlockHeaders().size();
assertEquals(nBlocks, 6);
@@ -230,87 +219,32 @@ public class BitcoinSerializerTest {
assertEquals(thirdBlock.getNonce(), 2850094635L);
}
@Test
public void testDeserializePayload() {
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
ByteArrayInputStream bais = new ByteArrayInputStream(Hex.decode("000000000000000000000000000000020000000000"));
BitcoinSerializer.BitcoinPacketHeader bitcoinPacketHeader = null;
// Test socket is disconnected.
InputStream inputStream = new InputStream() {
@Override
public int read() throws IOException {
return -1;
}
};
try {
bitcoinPacketHeader = new BitcoinSerializer.BitcoinPacketHeader(bais);
} catch (ProtocolException e) {
fail();
} catch (IOException e) {
fail();
}
try {
bs.deserializePayload(bitcoinPacketHeader, inputStream);
fail();
} catch (ProtocolException e) {
fail();
} catch (IOException e) {
// expected
}
// TODO Test protocol exception in deserializePayload.
}
@Test
public void testBitcoinPacketHeader() {
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
ByteArrayInputStream bais = new ByteArrayInputStream(new byte[]{});
BitcoinSerializer.BitcoinPacketHeader bitcoinPacketHeader;
try {
bitcoinPacketHeader = new BitcoinSerializer.BitcoinPacketHeader(bais);
} catch (ProtocolException e) {
new BitcoinSerializer.BitcoinPacketHeader(ByteBuffer.wrap(new byte[]{0}));
fail();
} catch (IOException e) {
// expected
} catch (BufferUnderflowException e) {
}
// Message with a Message size which is 1 too big, in little endian format.
byte[] wrongMessageLength = Hex.decode("000000000000000000000000010000020000000000");
bais = new ByteArrayInputStream(wrongMessageLength);
try {
bitcoinPacketHeader = new BitcoinSerializer.BitcoinPacketHeader(bais);
new BitcoinSerializer.BitcoinPacketHeader(ByteBuffer.wrap(wrongMessageLength));
fail();
} catch (ProtocolException e) {
// expected
} catch (IOException e) {
fail();
}
}
@Test
public void testSeekPastMagicBytes() {
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
// Empty byte stream, should give IOException.
ByteArrayInputStream bais = new ByteArrayInputStream(new byte[]{});
try {
bs.seekPastMagicBytes(bais);
fail();
} catch (IOException e) {
// expected
}
// Fail in another way, there is data in the stream but no magic bytes.
byte[] brokenMessage = Hex.decode("000000");
bais = new ByteArrayInputStream(brokenMessage);
try {
bs.seekPastMagicBytes(bais);
new BitcoinSerializer(MainNetParams.get()).seekPastMagicBytes(ByteBuffer.wrap(brokenMessage));
fail();
} catch (IOException e) {
} catch (BufferUnderflowException e) {
// expected
}
}

View File

@@ -1,61 +0,0 @@
package com.google.bitcoin.core;
import org.jboss.netty.channel.*;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class FakeChannel extends AbstractChannel {
final BlockingQueue<ChannelEvent> events = new ArrayBlockingQueue<ChannelEvent>(1000);
private final ChannelConfig config;
private SocketAddress localAddress;
private SocketAddress remoteAddress;
protected FakeChannel(ChannelFactory factory, ChannelPipeline pipeline, ChannelSink sink) {
super(null, factory, pipeline, sink);
config = new DefaultChannelConfig();
localAddress = new InetSocketAddress("127.0.0.1", 2000);
}
public ChannelConfig getConfig() {
return config;
}
public SocketAddress getLocalAddress() {
return localAddress;
}
public SocketAddress getRemoteAddress() {
return remoteAddress;
}
@Override
public ChannelFuture connect(SocketAddress remoteAddress) {
this.remoteAddress = remoteAddress;
return super.connect(remoteAddress);
}
public boolean isBound() {
return true;
}
public boolean isConnected() {
return true;
}
public ChannelEvent nextEvent() {
return events.poll();
}
public ChannelEvent nextEventBlocking() throws InterruptedException {
return events.take();
}
@Override
public boolean setClosed() {
return super.setClosed();
}
}

View File

@@ -1,54 +0,0 @@
package com.google.bitcoin.core;
import org.jboss.netty.channel.*;
import static org.jboss.netty.channel.Channels.fireChannelConnected;
public class FakeChannelSink extends AbstractChannelSink {
public void eventSunk(ChannelPipeline pipeline, ChannelEvent e) throws Exception {
if (e instanceof ChannelStateEvent) {
ChannelStateEvent event = (ChannelStateEvent) e;
FakeChannel channel = (FakeChannel) event.getChannel();
boolean offered = channel.events.offer(event);
assert offered;
ChannelFuture future = event.getFuture();
ChannelState state = event.getState();
Object value = event.getValue();
switch (state) {
case OPEN:
if (Boolean.FALSE.equals(value)) {
channel.setClosed();
}
break;
case BOUND:
if (value != null) {
// Bind
} else {
// Close
}
break;
case CONNECTED:
if (value != null) {
future.setSuccess();
fireChannelConnected(channel, channel.getRemoteAddress());
} else {
// Close
}
break;
case INTEREST_OPS:
// Unsupported - discard silently.
future.setSuccess();
break;
}
} else if (e instanceof MessageEvent) {
MessageEvent event = (MessageEvent) e;
FakeChannel channel = (FakeChannel) event.getChannel();
boolean offered = channel.events.offer(event);
assert offered;
event.getFuture().setSuccess();
}
}
}

View File

@@ -4,17 +4,31 @@ import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.params.UnitTestParams;
import com.google.bitcoin.store.MemoryBlockStore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.spongycastle.util.encoders.Hex;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(value = Parameterized.class)
public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
@Parameterized.Parameters
public static Collection<ClientType[]> parameters() {
return Arrays.asList(new ClientType[] {ClientType.NIO_CLIENT_MANAGER},
new ClientType[] {ClientType.BLOCKING_CLIENT_MANAGER});
}
public FilteredBlockAndPartialMerkleTreeTests(ClientType clientType) {
super(clientType);
}
@Test
// Simple deserialization sanity check
public void deserializeFilteredBlock() throws Exception {
@@ -87,10 +101,10 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
peerGroup.addWallet(wallet);
blockChain.addWallet(wallet);
peerGroup.start();
peerGroup.startAndWait();
// Create a peer.
FakeChannel p1 = connectPeer(1);
InboundMessageQueuer p1 = connectPeer(1);
assertEquals(1, peerGroup.numConnectedPeers());
// Send an inv for block 100001
InventoryMessage inv = new InventoryMessage(unitTestParams);
@@ -115,7 +129,9 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
inbound(p1, tx2);
inbound(p1, tx3);
inbound(p1, new Pong(((Ping)ping).getNonce()));
pingAndWait(p1);
Set<Transaction> transactions = wallet.getTransactions(false);
assertTrue(transactions.size() == 4);
for (Transaction tx : transactions) {
@@ -128,5 +144,6 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
// Peer 1 goes away.
closePeer(peerOf(p1));
peerGroup.stop();
super.tearDown();
}
}

View File

@@ -0,0 +1,44 @@
package com.google.bitcoin.core;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import com.google.common.util.concurrent.SettableFuture;
/**
* An extension of {@link PeerSocketHandler} that keeps inbound messages in a queue for later processing
*/
public abstract class InboundMessageQueuer extends PeerSocketHandler {
final BlockingQueue<Message> inboundMessages = new ArrayBlockingQueue<Message>(1000);
final Map<Long, SettableFuture<Void>> mapPingFutures = new HashMap<Long, SettableFuture<Void>>();
public Peer peer;
protected InboundMessageQueuer(NetworkParameters params) {
super(params, new InetSocketAddress("127.0.0.1", 2000));
}
public Message nextMessage() {
return inboundMessages.poll();
}
public Message nextMessageBlocking() throws InterruptedException {
return inboundMessages.take();
}
@Override
protected void processMessage(Message m) throws Exception {
if (m instanceof Ping) {
SettableFuture<Void> future = mapPingFutures.get(((Ping)m).getNonce());
if (future != null) {
future.set(null);
return;
}
}
inboundMessages.offer(m);
}
}

View File

@@ -24,8 +24,8 @@ import org.junit.Before;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static com.google.bitcoin.utils.TestUtils.createFakeBlock;
@@ -179,8 +179,8 @@ public class LazyParseByteCacheTest {
BitcoinSerializer bs = new BitcoinSerializer(unitTestParams, lazy, retain);
Block b1;
Block bRef;
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
//verify our reference BitcoinSerializer produces matching byte array.
bos.reset();
@@ -231,8 +231,8 @@ public class LazyParseByteCacheTest {
}
//refresh block
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
//retrieve a value from header
b1.getDifficultyTarget();
@@ -244,8 +244,8 @@ public class LazyParseByteCacheTest {
//refresh block
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
//retrieve a value from a child and header
b1.getDifficultyTarget();
@@ -270,8 +270,8 @@ public class LazyParseByteCacheTest {
serDeser(bs, b1, bos.toByteArray(), null, null);
//refresh block
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
//change a value in header
b1.setNonce(23);
@@ -289,8 +289,8 @@ public class LazyParseByteCacheTest {
serDeser(bs, b1, bos.toByteArray(), null, null);
//refresh block
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
//retrieve a value from a child of a child
b1.getTransactions();
@@ -313,8 +313,8 @@ public class LazyParseByteCacheTest {
}
//refresh block
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
//add an input
b1.getTransactions();
@@ -357,10 +357,10 @@ public class LazyParseByteCacheTest {
}
//refresh block
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
Block b2 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
Block bRef2 = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
Block b2 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
Block bRef2 = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
//reparent an input
b1.getTransactions();
@@ -397,7 +397,7 @@ public class LazyParseByteCacheTest {
serDeser(bs, b1, bos.toByteArray(), null, null);
//how about if we refresh it?
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
bos.reset();
bsRef.serialize(bRef, bos);
serDeser(bs, b1, bos.toByteArray(), null, null);
@@ -406,7 +406,7 @@ public class LazyParseByteCacheTest {
}
public void testTransaction(NetworkParameters params, byte[] txBytes, boolean isChild, boolean lazy, boolean retain) throws Exception {
//reference serializer to produce comparison serialization output after changes to
//message structure.
BitcoinSerializer bsRef = new BitcoinSerializer(params, false, false);
@@ -415,8 +415,8 @@ public class LazyParseByteCacheTest {
BitcoinSerializer bs = new BitcoinSerializer(params, lazy, retain);
Transaction t1;
Transaction tRef;
t1 = (Transaction) bs.deserialize(new ByteArrayInputStream(txBytes));
tRef = (Transaction) bsRef.deserialize(new ByteArrayInputStream(txBytes));
t1 = (Transaction) bs.deserialize(ByteBuffer.wrap(txBytes));
tRef = (Transaction) bsRef.deserialize(ByteBuffer.wrap(txBytes));
//verify our reference BitcoinSerializer produces matching byte array.
bos.reset();
@@ -454,8 +454,8 @@ public class LazyParseByteCacheTest {
}
//refresh tx
t1 = (Transaction) bs.deserialize(new ByteArrayInputStream(txBytes));
tRef = (Transaction) bsRef.deserialize(new ByteArrayInputStream(txBytes));
t1 = (Transaction) bs.deserialize(ByteBuffer.wrap(txBytes));
tRef = (Transaction) bsRef.deserialize(ByteBuffer.wrap(txBytes));
//add an input
if (t1.getInputs().size() > 0) {
@@ -482,7 +482,7 @@ public class LazyParseByteCacheTest {
bs.serialize(message, bos);
byte[] b1 = bos.toByteArray();
Message m2 = bs.deserialize(new ByteArrayInputStream(b1));
Message m2 = bs.deserialize(ByteBuffer.wrap(b1));
assertEquals(message, m2);

View File

@@ -1,166 +0,0 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.core;
import java.io.IOException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/** Allows messages to be inserted and removed in a thread-safe manner. */
public class MockNetworkConnection implements NetworkConnection {
private BlockingQueue<Object> inboundMessageQ;
private BlockingQueue<Message> outboundMessageQ;
private boolean waitingToRead;
// Not used for anything except marking the shutdown point in the inbound queue.
private Object disconnectMarker = new Object();
private VersionMessage versionMessage;
private static int fakePort = 1;
private PeerAddress peerAddress;
public MockNetworkConnection() {
}
public void connect(PeerAddress peerAddress, int connectTimeoutMsec) {
inboundMessageQ = new ArrayBlockingQueue<Object>(10);
outboundMessageQ = new ArrayBlockingQueue<Message>(10);
this.peerAddress = peerAddress;
}
public void ping() throws IOException {
}
public void shutdown() throws IOException {
inboundMessageQ.add(disconnectMarker);
}
public synchronized void disconnect() throws IOException {
inboundMessageQ.add(disconnectMarker);
}
public void exceptionOnRead(Exception e) {
inboundMessageQ.add(e);
}
public Message readMessage() throws IOException, ProtocolException {
try {
// Notify popOutbound() that the network thread is now waiting to receive input. This is needed because
// otherwise it's impossible to tell apart "thread decided to not write any message" from "thread is still
// working on it".
synchronized (this) {
waitingToRead = true;
notifyAll();
}
Object o = inboundMessageQ.take();
// BUG 141: There is a race at this point: inbound queue can be empty at the same time as waitingToRead is
// true, which is taken as an indication that all messages have been processed. In fact they have not.
synchronized (this) {
waitingToRead = false;
}
if (o instanceof IOException) {
throw (IOException) o;
} else if (o instanceof ProtocolException) {
throw (ProtocolException) o;
} else if (o instanceof Message) {
return (Message) o;
} else if (o == disconnectMarker) {
throw new IOException("done");
} else {
throw new RuntimeException("Unknown object in inbound queue.");
}
} catch (InterruptedException e) {
throw new IOException(e.getMessage());
}
}
public void writeMessage(Message message) throws IOException {
try {
outboundMessageQ.put(message);
} catch (InterruptedException e) {
throw new IOException(e.getMessage());
}
}
public void setVersionMessage(VersionMessage msg) {
this.versionMessage = msg;
}
public VersionMessage getVersionMessage() {
if (versionMessage == null) throw new RuntimeException("Need to call setVersionMessage first");
return versionMessage;
}
public PeerAddress getPeerAddress() {
return peerAddress;
}
public void close() {
}
/** Call this to add a message which will be received by the NetworkConnection user. Wakes up the network thread. */
public void inbound(Message m) {
try {
inboundMessageQ.put(m);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* Returns a message that has been written with writeMessage. Waits until the peer thread is sitting inside
* readMessage() and has no further inbound messages to process. If at that point there is a message in the outbound
* queue, takes and returns it. Otherwise returns null. Use popOutbound() for when there is no other thread.
*/
public Message outbound() throws InterruptedException {
synchronized (this) {
while (!waitingToRead || inboundMessageQ.size() > 0) {
wait();
}
}
return popOutbound();
}
/**
* Takes the most recently sent message or returns NULL if there are none waiting.
*/
public Message popOutbound() throws InterruptedException {
if (outboundMessageQ.peek() != null)
return outboundMessageQ.take();
else
return null;
}
/**
* Takes the most recently received message or returns NULL if there are none waiting.
*/
public Object popInbound() throws InterruptedException {
if (inboundMessageQ.peek() != null)
return inboundMessageQ.take();
else
return null;
}
/** Convenience that does an inbound() followed by returning the value of outbound() */
public Message exchange(Message m) throws InterruptedException {
inbound(m);
return outbound();
}
}

View File

@@ -22,23 +22,39 @@ import com.google.bitcoin.params.UnitTestParams;
import com.google.bitcoin.store.MemoryBlockStore;
import com.google.bitcoin.utils.TestUtils;
import com.google.bitcoin.utils.Threading;
import com.google.common.util.concurrent.SettableFuture;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.util.HashSet;
import java.util.Set;
import java.util.*;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.*;
// TX announcement and broadcast is tested in TransactionBroadcastTest.
@RunWith(value = Parameterized.class)
public class PeerGroupTest extends TestWithPeerGroup {
static final NetworkParameters params = UnitTestParams.get();
@Parameterized.Parameters
public static Collection<ClientType[]> parameters() {
return Arrays.asList(new ClientType[] {ClientType.NIO_CLIENT_MANAGER},
new ClientType[] {ClientType.BLOCKING_CLIENT_MANAGER});
}
public PeerGroupTest(ClientType clientType) {
super(clientType);
}
@Override
@Before
public void setUp() throws Exception {
@@ -54,10 +70,62 @@ public class PeerGroupTest extends TestWithPeerGroup {
@Test
public void listener() throws Exception {
final AtomicInteger connectedPeers = new AtomicInteger(0);
final AtomicInteger disconnectedPeers = new AtomicInteger(0);
final SettableFuture<Void> firstDisconnectFuture = SettableFuture.create();
final SettableFuture<Void> secondDisconnectFuture = SettableFuture.create();
final Map<Peer, AtomicInteger> peerToMessageCount = new HashMap<Peer, AtomicInteger>();
AbstractPeerEventListener listener = new AbstractPeerEventListener() {
@Override
public void onPeerConnected(Peer peer, int peerCount) {
connectedPeers.incrementAndGet();
}
@Override
public void onPeerDisconnected(Peer peer, int peerCount) {
if (disconnectedPeers.incrementAndGet() == 1)
firstDisconnectFuture.set(null);
else
secondDisconnectFuture.set(null);
}
@Override
public Message onPreMessageReceived(Peer peer, Message m) {
AtomicInteger messageCount = peerToMessageCount.get(peer);
if (messageCount == null) {
messageCount = new AtomicInteger(0);
peerToMessageCount.put(peer, messageCount);
}
messageCount.incrementAndGet();
// Just pass the message right through for further processing.
return m;
}
};
peerGroup.startAndWait();
peerGroup.addEventListener(listener);
// Create a couple of peers.
InboundMessageQueuer p1 = connectPeer(1);
Threading.waitForUserCode();
assertEquals(1, connectedPeers.get());
InboundMessageQueuer p2 = connectPeer(2);
Threading.waitForUserCode();
assertEquals(2, connectedPeers.get());
pingAndWait(p1);
pingAndWait(p2);
Threading.waitForUserCode();
assertEquals(0, disconnectedPeers.get());
p1.close();
firstDisconnectFuture.get();
assertEquals(1, disconnectedPeers.get());
p2.close();
secondDisconnectFuture.get();
assertEquals(2, disconnectedPeers.get());
assertTrue(peerGroup.removeEventListener(listener));
assertFalse(peerGroup.removeEventListener(listener));
}
@Test
@@ -94,8 +162,8 @@ public class PeerGroupTest extends TestWithPeerGroup {
peerGroup.startAndWait();
// Create a couple of peers.
FakeChannel p1 = connectPeer(1);
FakeChannel p2 = connectPeer(2);
InboundMessageQueuer p1 = connectPeer(1);
InboundMessageQueuer p2 = connectPeer(2);
// Check the peer accessors.
assertEquals(2, peerGroup.numConnectedPeers());
@@ -122,6 +190,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
GetDataMessage getdata = (GetDataMessage) outbound(p2);
assertNotNull(getdata);
inbound(p2, new NotFoundMessage(unitTestParams, getdata.getItems()));
pingAndWait(p2);
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
peerGroup.stopAndWait();
}
@@ -132,8 +201,8 @@ public class PeerGroupTest extends TestWithPeerGroup {
peerGroup.startAndWait();
// Create a couple of peers.
FakeChannel p1 = connectPeer(1);
FakeChannel p2 = connectPeer(2);
InboundMessageQueuer p1 = connectPeer(1);
InboundMessageQueuer p2 = connectPeer(2);
assertEquals(2, peerGroup.numConnectedPeers());
// Set up a little block chain. We heard about b1 but not b2 (it is pending download). b3 is solved whilst we
@@ -148,11 +217,20 @@ public class PeerGroupTest extends TestWithPeerGroup {
inv.addBlock(b3);
// Only peer 1 tries to download it.
inbound(p1, inv);
pingAndWait(p1);
assertTrue(outbound(p1) instanceof GetDataMessage);
assertNull(outbound(p2));
// Peer 1 goes away, peer 2 becomes the download peer and thus queries the remote mempool.
final SettableFuture<Void> p1CloseFuture = SettableFuture.create();
peerOf(p1).addEventListener(new AbstractPeerEventListener() {
@Override
public void onPeerDisconnected(Peer peer, int peerCount) {
p1CloseFuture.set(null);
}
});
closePeer(peerOf(p1));
p1CloseFuture.get();
// Peer 2 fetches it next time it hears an inv (should it fetch immediately?).
inbound(p2, inv);
assertTrue(outbound(p2) instanceof GetDataMessage);
@@ -167,7 +245,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
peerGroup.startAndWait();
// Create a couple of peers.
FakeChannel p1 = connectPeer(1);
InboundMessageQueuer p1 = connectPeer(1);
// Set up a little block chain.
Block b1 = TestUtils.createFakeBlock(blockStore).block;
@@ -190,7 +268,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
// We hand back the first block.
inbound(p1, b1);
// Now we successfully connect to another peer. There should be no messages sent.
FakeChannel p2 = connectPeer(2);
InboundMessageQueuer p2 = connectPeer(2);
Message message = (Message)outbound(p2);
assertNull(message == null ? "" : message.toString(), message);
peerGroup.stop();
@@ -200,6 +278,8 @@ public class PeerGroupTest extends TestWithPeerGroup {
public void transactionConfidence() throws Exception {
// Checks that we correctly count how many peers broadcast a transaction, so we can establish some measure of
// its trustworthyness assuming an untampered with internet connection.
peerGroup.startAndWait();
final Transaction[] event = new Transaction[2];
peerGroup.addEventListener(new AbstractPeerEventListener() {
@Override
@@ -208,9 +288,9 @@ public class PeerGroupTest extends TestWithPeerGroup {
}
}, Threading.SAME_THREAD);
FakeChannel p1 = connectPeer(1);
FakeChannel p2 = connectPeer(2);
FakeChannel p3 = connectPeer(3);
InboundMessageQueuer p1 = connectPeer(1);
InboundMessageQueuer p2 = connectPeer(2);
InboundMessageQueuer p3 = connectPeer(3);
Transaction tx = TestUtils.createFakeTx(params, Utils.toNanoCoins(20, 0), address);
InventoryMessage inv = new InventoryMessage(params);
@@ -247,6 +327,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
});
// A straggler reports in.
inbound(p3, inv);
pingAndWait(p3);
Threading.waitForUserCode();
assertEquals(tx, event[1]);
assertEquals(3, tx.getConfidence().numBroadcastPeers());
@@ -280,8 +361,10 @@ public class PeerGroupTest extends TestWithPeerGroup {
peerGroup.startAndWait();
peerGroup.setPingIntervalMsec(0);
VersionMessage versionMessage = new VersionMessage(params, 2);
versionMessage.clientVersion = Pong.MIN_PROTOCOL_VERSION;
versionMessage.clientVersion = FilteredBlock.MIN_PROTOCOL_VERSION;
versionMessage.localServices = VersionMessage.NODE_NETWORK;
connectPeer(1, versionMessage);
peerGroup.waitForPeers(1).get();
assertFalse(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE);
}
@@ -290,10 +373,12 @@ public class PeerGroupTest extends TestWithPeerGroup {
peerGroup.startAndWait();
peerGroup.setPingIntervalMsec(100);
VersionMessage versionMessage = new VersionMessage(params, 2);
versionMessage.clientVersion = Pong.MIN_PROTOCOL_VERSION;
FakeChannel p1 = connectPeer(1, versionMessage);
versionMessage.clientVersion = FilteredBlock.MIN_PROTOCOL_VERSION;
versionMessage.localServices = VersionMessage.NODE_NETWORK;
InboundMessageQueuer p1 = connectPeer(1, versionMessage);
Ping ping = (Ping) outbound(p1);
inbound(p1, new Pong(ping.getNonce()));
pingAndWait(p1);
assertTrue(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE);
// The call to outbound should block until a ping arrives.
ping = (Ping) waitForOutbound(p1);
@@ -305,26 +390,53 @@ public class PeerGroupTest extends TestWithPeerGroup {
public void downloadPeerSelection() throws Exception {
peerGroup.startAndWait();
VersionMessage versionMessage2 = new VersionMessage(params, 2);
versionMessage2.clientVersion = 60000;
versionMessage2.clientVersion = FilteredBlock.MIN_PROTOCOL_VERSION;
versionMessage2.localServices = VersionMessage.NODE_NETWORK;
VersionMessage versionMessage3 = new VersionMessage(params, 3);
versionMessage3.clientVersion = 60000;
versionMessage3.clientVersion = FilteredBlock.MIN_PROTOCOL_VERSION;
versionMessage3.localServices = VersionMessage.NODE_NETWORK;
assertNull(peerGroup.getDownloadPeer());
Peer a = PeerGroup.peerFromChannel(connectPeer(1, versionMessage2));
Peer a = connectPeer(1, versionMessage2).peer;
assertEquals(2, peerGroup.getMostCommonChainHeight());
assertEquals(a, peerGroup.getDownloadPeer());
PeerGroup.peerFromChannel(connectPeer(2, versionMessage2));
connectPeer(2, versionMessage2);
assertEquals(2, peerGroup.getMostCommonChainHeight());
assertEquals(a, peerGroup.getDownloadPeer()); // No change.
Peer c = PeerGroup.peerFromChannel(connectPeer(3, versionMessage3));
Peer c = connectPeer(3, versionMessage3).peer;
assertEquals(2, peerGroup.getMostCommonChainHeight());
assertEquals(a, peerGroup.getDownloadPeer()); // No change yet.
PeerGroup.peerFromChannel(connectPeer(4, versionMessage3));
connectPeer(4, versionMessage3);
assertEquals(3, peerGroup.getMostCommonChainHeight());
assertEquals(c, peerGroup.getDownloadPeer()); // Switch to first peer advertising new height.
// New peer with a higher protocol version but same chain height.
VersionMessage versionMessage4 = new VersionMessage(params, 3);
//TODO: When PeerGroup.selectDownloadPeer.PREFERRED_VERSION is not equal to vMinRequiredProtocolVersion,
// reenable this test
/*VersionMessage versionMessage4 = new VersionMessage(params, 3);
versionMessage4.clientVersion = 100000;
Peer d = PeerGroup.peerFromChannel(connectPeer(5, versionMessage4));
assertEquals(d, peerGroup.getDownloadPeer());
versionMessage4.localServices = VersionMessage.NODE_NETWORK;
InboundMessageQueuer d = connectPeer(5, versionMessage4);
assertEquals(d.peer, peerGroup.getDownloadPeer());*/
}
@Test
public void peerTimeoutTest() throws Exception {
peerGroup.startAndWait();
peerGroup.setConnectTimeoutMillis(100);
final SettableFuture<Void> peerConnectedFuture = SettableFuture.create();
final SettableFuture<Void> peerDisconnectedFuture = SettableFuture.create();
peerGroup.addEventListener(new AbstractPeerEventListener() {
@Override public void onPeerConnected(Peer peer, int peerCount) {
peerConnectedFuture.set(null);
}
@Override public void onPeerDisconnected(Peer peer, int peerCount) {
peerDisconnectedFuture.set(null);
}
}, Threading.SAME_THREAD);
connectPeerWithoutVersionExchange(0);
Thread.sleep(50);
assertFalse(peerConnectedFuture.isDone() || peerDisconnectedFuture.isDone());
Thread.sleep(60);
assertTrue(!peerConnectedFuture.isDone() && peerDisconnectedFuture.isDone());
}
}

View File

@@ -16,44 +16,58 @@
package com.google.bitcoin.core;
import com.google.bitcoin.core.Peer.PeerHandler;
import com.google.bitcoin.params.TestNet3Params;
import com.google.bitcoin.utils.TestUtils;
import com.google.bitcoin.utils.Threading;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.easymock.Capture;
import org.easymock.CaptureType;
import org.jboss.netty.channel.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static com.google.bitcoin.utils.TestUtils.*;
import static org.easymock.EasyMock.*;
import static org.junit.Assert.*;
@RunWith(value = Parameterized.class)
public class PeerTest extends TestWithNetworkConnections {
private Peer peer;
private Capture<DownstreamMessageEvent> event;
private PeerHandler handler;
private InboundMessageQueuer writeTarget;
private static final int OTHER_PEER_CHAIN_HEIGHT = 110;
private MemoryPool memoryPool;
private final AtomicBoolean fail = new AtomicBoolean(false);
@Parameterized.Parameters
public static Collection<ClientType[]> parameters() {
return Arrays.asList(new ClientType[] {ClientType.NIO_CLIENT_MANAGER},
new ClientType[] {ClientType.BLOCKING_CLIENT_MANAGER},
new ClientType[] {ClientType.NIO_CLIENT},
new ClientType[] {ClientType.BLOCKING_CLIENT});
}
public PeerTest(ClientType clientType) {
super(clientType);
}
@Override
@Before
@@ -62,62 +76,35 @@ public class PeerTest extends TestWithNetworkConnections {
memoryPool = new MemoryPool();
VersionMessage ver = new VersionMessage(unitTestParams, 100);
peer = new Peer(unitTestParams, blockChain, ver, memoryPool);
peer = new Peer(unitTestParams, ver, new InetSocketAddress("127.0.0.1", 4000), blockChain, memoryPool);
peer.addWallet(wallet);
handler = peer.getHandler();
event = new Capture<DownstreamMessageEvent>(CaptureType.ALL);
pipeline.sendDownstream(capture(event));
expectLastCall().anyTimes();
}
@After
public void tearDown() throws Exception {
super.tearDown();
assertFalse(fail.get());
}
private void connect() throws Exception {
connect(handler, channel, ctx, 70001);
connectWithVersion(70001);
}
private void connectWithVersion(int version) throws Exception {
connect(handler, channel, ctx, version);
}
private void connect(PeerHandler handler, Channel channel, ChannelHandlerContext ctx, int version) throws Exception {
handler.connectRequested(ctx, new UpstreamChannelStateEvent(channel, ChannelState.CONNECTED, socketAddress));
VersionMessage peerVersion = new VersionMessage(unitTestParams, OTHER_PEER_CHAIN_HEIGHT);
peerVersion.clientVersion = version;
DownstreamMessageEvent versionEvent =
new DownstreamMessageEvent(channel, Channels.future(channel), peerVersion, null);
handler.messageReceived(ctx, versionEvent);
peerVersion.localServices = VersionMessage.NODE_NETWORK;
writeTarget = connect(peer, peerVersion);
}
@Test
public void testAddEventListener() throws Exception {
control.replay();
connect();
PeerEventListener listener = new AbstractPeerEventListener();
peer.addEventListener(listener);
assertTrue(peer.removeEventListener(listener));
assertFalse(peer.removeEventListener(listener));
}
// Check that the connection is shut down if there's a read error and that the exception is propagated.
@Test
public void testRun_exception() throws Exception {
expect(channel.close()).andReturn(null);
control.replay();
handler.exceptionCaught(ctx,
new DefaultExceptionEvent(channel, new IOException("proto")));
control.verify();
}
@Test
public void testRun_protocolException() throws Exception {
expect(channel.close()).andReturn(null);
replay(channel);
handler.exceptionCaught(ctx,
new DefaultExceptionEvent(channel, new ProtocolException("proto")));
verify(channel);
}
// Check that it runs through the event loop and shut down correctly
@Test
@@ -135,12 +122,10 @@ public class PeerTest extends TestWithNetworkConnections {
Block b4 = makeSolvedTestBlock(b3);
Block b5 = makeSolvedTestBlock(b4);
control.replay();
connect();
peer.startBlockChainDownload();
GetBlocksMessage getblocks = (GetBlocksMessage)outbound();
GetBlocksMessage getblocks = (GetBlocksMessage)outbound(writeTarget);
assertEquals(blockStore.getChainHead().getHeader().getHash(), getblocks.getLocator().get(0));
assertEquals(Sha256Hash.ZERO_HASH, getblocks.getStopHash());
// Remote peer sends us an inv with some blocks.
@@ -148,27 +133,27 @@ public class PeerTest extends TestWithNetworkConnections {
inv.addBlock(b2);
inv.addBlock(b3);
// We do a getdata on them.
inbound(peer, inv);
GetDataMessage getdata = (GetDataMessage)outbound();
inbound(writeTarget, inv);
GetDataMessage getdata = (GetDataMessage)outbound(writeTarget);
assertEquals(b2.getHash(), getdata.getItems().get(0).hash);
assertEquals(b3.getHash(), getdata.getItems().get(1).hash);
assertEquals(2, getdata.getItems().size());
// Remote peer sends us the blocks. The act of doing a getdata for b3 results in getting an inv with just the
// best chain head in it.
inbound(peer, b2);
inbound(peer, b3);
inbound(writeTarget, b2);
inbound(writeTarget, b3);
inv = new InventoryMessage(unitTestParams);
inv.addBlock(b5);
// We request the head block.
inbound(peer, inv);
getdata = (GetDataMessage)outbound();
inbound(writeTarget, inv);
getdata = (GetDataMessage)outbound(writeTarget);
assertEquals(b5.getHash(), getdata.getItems().get(0).hash);
assertEquals(1, getdata.getItems().size());
// Peer sends us the head block. The act of receiving the orphan block triggers a getblocks to fill in the
// rest of the chain.
inbound(peer, b5);
getblocks = (GetBlocksMessage)outbound();
inbound(writeTarget, b5);
getblocks = (GetBlocksMessage)outbound(writeTarget);
assertEquals(b5.getHash(), getblocks.getStopHash());
assertEquals(b3.getHash(), getblocks.getLocator().get(0));
// At this point another block is solved and broadcast. The inv triggers a getdata but we do NOT send another
@@ -179,33 +164,31 @@ public class PeerTest extends TestWithNetworkConnections {
Block b6 = makeSolvedTestBlock(b5);
inv = new InventoryMessage(unitTestParams);
inv.addBlock(b6);
inbound(peer, inv);
getdata = (GetDataMessage)outbound();
inbound(writeTarget, inv);
getdata = (GetDataMessage)outbound(writeTarget);
assertEquals(1, getdata.getItems().size());
assertEquals(b6.getHash(), getdata.getItems().get(0).hash);
inbound(peer, b6);
assertFalse(event.hasCaptured()); // Nothing is sent at this point.
inbound(writeTarget, b6);
assertNull(outbound(writeTarget)); // Nothing is sent at this point.
// We're still waiting for the response to the getblocks (b3,b5) sent above.
inv = new InventoryMessage(unitTestParams);
inv.addBlock(b4);
inv.addBlock(b5);
inbound(peer, inv);
getdata = (GetDataMessage)outbound();
inbound(writeTarget, inv);
getdata = (GetDataMessage)outbound(writeTarget);
assertEquals(1, getdata.getItems().size());
assertEquals(b4.getHash(), getdata.getItems().get(0).hash);
// We already have b5 from before, so it's not requested again.
inbound(peer, b4);
assertFalse(event.hasCaptured());
inbound(writeTarget, b4);
assertNull(outbound(writeTarget));
// b5 and b6 are now connected by the block chain and we're done.
assertNull(outbound(writeTarget));
closePeer(peer);
control.verify();
}
// Check that an inventory tickle is processed correctly when downloading missing blocks is active.
@Test
public void invTickle() throws Exception {
control.replay();
connect();
Block b1 = createFakeBlock(blockStore).block;
@@ -213,20 +196,20 @@ public class PeerTest extends TestWithNetworkConnections {
// Make a missing block.
Block b2 = makeSolvedTestBlock(b1);
Block b3 = makeSolvedTestBlock(b2);
inbound(peer, b3);
inbound(writeTarget, b3);
InventoryMessage inv = new InventoryMessage(unitTestParams);
InventoryItem item = new InventoryItem(InventoryItem.Type.Block, b3.getHash());
inv.addItem(item);
inbound(peer, inv);
inbound(writeTarget, inv);
GetBlocksMessage getblocks = (GetBlocksMessage)outbound();
GetBlocksMessage getblocks = (GetBlocksMessage)outbound(writeTarget);
List<Sha256Hash> expectedLocator = new ArrayList<Sha256Hash>();
expectedLocator.add(b1.getHash());
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
assertEquals(getblocks.getLocator(), expectedLocator);
assertEquals(getblocks.getStopHash(), b3.getHash());
control.verify();
assertNull(outbound(writeTarget));
}
// Check that an inv to a peer that is not set to download missing blocks does nothing.
@@ -234,9 +217,7 @@ public class PeerTest extends TestWithNetworkConnections {
public void invNoDownload() throws Exception {
// Don't download missing blocks.
peer.setDownloadData(false);
control.replay();
connect();
// Make a missing block that we receive.
@@ -248,16 +229,14 @@ public class PeerTest extends TestWithNetworkConnections {
InventoryMessage inv = new InventoryMessage(unitTestParams);
InventoryItem item = new InventoryItem(InventoryItem.Type.Block, b2.getHash());
inv.addItem(item);
inbound(peer, inv);
inbound(writeTarget, inv);
// Peer does nothing with it.
control.verify();
assertNull(outbound(writeTarget));
}
@Test
public void invDownloadTx() throws Exception {
control.replay();
connect();
peer.setDownloadData(true);
@@ -267,34 +246,31 @@ public class PeerTest extends TestWithNetworkConnections {
InventoryMessage inv = new InventoryMessage(unitTestParams);
InventoryItem item = new InventoryItem(InventoryItem.Type.Transaction, tx.getHash());
inv.addItem(item);
inbound(peer, inv);
inbound(writeTarget, inv);
// Peer hasn't seen it before, so will ask for it.
GetDataMessage getdata = (GetDataMessage) outbound();
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(1, getdata.getItems().size());
assertEquals(tx.getHash(), getdata.getItems().get(0).hash);
inbound(peer, tx);
inbound(writeTarget, tx);
// Ask for the dependency, it's not in the mempool (in chain).
getdata = (GetDataMessage) outbound();
inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
getdata = (GetDataMessage) outbound(writeTarget);
inbound(writeTarget, new NotFoundMessage(unitTestParams, getdata.getItems()));
pingAndWait(writeTarget);
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@Test
public void invDownloadTxMultiPeer() throws Exception {
ChannelHandlerContext ctx2 = createChannelHandlerContext();
Channel channel2 = createChannel();
createPipeline(channel2);
control.replay();
// Check co-ordination of which peer to download via the memory pool.
MockNetworkConnection conn2 = createMockNetworkConnection();
VersionMessage ver = new VersionMessage(unitTestParams, 100);
Peer peer2 = new Peer(unitTestParams, blockChain, ver, memoryPool);
Peer peer2 = new Peer(unitTestParams, ver, new InetSocketAddress("127.0.0.1", 4242), blockChain, memoryPool);
peer2.addWallet(wallet);
VersionMessage peerVersion = new VersionMessage(unitTestParams, OTHER_PEER_CHAIN_HEIGHT);
peerVersion.clientVersion = 70001;
peerVersion.localServices = VersionMessage.NODE_NETWORK;
connect();
connect(peer2.getHandler(), channel2, ctx2, 70001);
InboundMessageQueuer writeTarget2 = connect(peer2, peerVersion);
// Make a tx and advertise it to one of the peers.
BigInteger value = Utils.toNanoCoins(1, 0);
@@ -303,51 +279,74 @@ public class PeerTest extends TestWithNetworkConnections {
InventoryItem item = new InventoryItem(InventoryItem.Type.Transaction, tx.getHash());
inv.addItem(item);
inbound(peer, inv);
inbound(writeTarget, inv);
// We got a getdata message.
GetDataMessage message = (GetDataMessage)outbound();
GetDataMessage message = (GetDataMessage)outbound(writeTarget);
assertEquals(1, message.getItems().size());
assertEquals(tx.getHash(), message.getItems().get(0).hash);
assertTrue(memoryPool.maybeWasSeen(tx.getHash()));
// Advertising to peer2 results in no getdata message.
conn2.inbound(inv);
assertFalse(event.hasCaptured());
inbound(writeTarget2, inv);
pingAndWait(writeTarget2);
assertNull(outbound(writeTarget2));
}
// Check that inventory message containing blocks we want is processed correctly.
@Test
public void newBlock() throws Exception {
PeerEventListener listener = control.createMock(PeerEventListener.class);
Block b1 = createFakeBlock(blockStore).block;
blockChain.add(b1);
Block b2 = makeSolvedTestBlock(b1);
final Block b2 = makeSolvedTestBlock(b1);
// Receive notification of a new block.
InventoryMessage inv = new InventoryMessage(unitTestParams);
final InventoryMessage inv = new InventoryMessage(unitTestParams);
InventoryItem item = new InventoryItem(InventoryItem.Type.Block, b2.getHash());
inv.addItem(item);
expect(listener.onPreMessageReceived(eq(peer), eq(inv))).andReturn(inv);
expect(listener.onPreMessageReceived(eq(peer), eq(b2))).andReturn(b2);
// The listener gets the delta between the first announced height and our height.
listener.onBlocksDownloaded(eq(peer), anyObject(Block.class), eq(OTHER_PEER_CHAIN_HEIGHT - 2));
expectLastCall();
control.replay();
final AtomicInteger newBlockMessagesReceived = new AtomicInteger(0);
connect();
peer.addEventListener(listener, Threading.SAME_THREAD);
// Round-trip a ping so that we never see the response verack if we attach too quick
pingAndWait(writeTarget);
peer.addEventListener(new AbstractPeerEventListener() {
@Override
public synchronized Message onPreMessageReceived(Peer p, Message m) {
if (p != peer)
fail.set(true);
if (m instanceof Pong)
return m;
int newValue = newBlockMessagesReceived.incrementAndGet();
if (newValue == 1 && !inv.equals(m))
fail.set(true);
else if (newValue == 2 && !b2.equals(m))
fail.set(true);
else if (newValue > 3)
fail.set(true);
return m;
}
@Override
public synchronized void onBlocksDownloaded(Peer p, Block block, int blocksLeft) {
int newValue = newBlockMessagesReceived.incrementAndGet();
if (newValue != 3 || p != peer || !block.equals(b2) || blocksLeft != OTHER_PEER_CHAIN_HEIGHT - 2)
fail.set(true);
}
}, Threading.SAME_THREAD);
long height = peer.getBestHeight();
inbound(peer, inv);
inbound(writeTarget, inv);
pingAndWait(writeTarget);
assertEquals(height + 1, peer.getBestHeight());
// Response to the getdata message.
inbound(peer, b2);
inbound(writeTarget, b2);
control.verify();
pingAndWait(writeTarget);
Threading.waitForUserCode();
pingAndWait(writeTarget);
assertEquals(3, newBlockMessagesReceived.get());
GetDataMessage getdata = (GetDataMessage) event.getValue().getMessage();
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
List<InventoryItem> items = getdata.getItems();
assertEquals(1, items.size());
assertEquals(b2.getHash(), items.get(0).hash);
@@ -357,38 +356,34 @@ public class PeerTest extends TestWithNetworkConnections {
// Check that it starts downloading the block chain correctly on request.
@Test
public void startBlockChainDownload() throws Exception {
PeerEventListener listener = control.createMock(PeerEventListener.class);
Block b1 = createFakeBlock(blockStore).block;
blockChain.add(b1);
Block b2 = makeSolvedTestBlock(b1);
blockChain.add(b2);
listener.onChainDownloadStarted(peer, 108);
expectLastCall();
control.replay();
connect();
peer.addEventListener(listener, Threading.SAME_THREAD);
fail.set(true);
peer.addEventListener(new AbstractPeerEventListener() {
@Override
public void onChainDownloadStarted(Peer p, int blocksLeft) {
if (p == peer && blocksLeft == 108)
fail.set(false);
}
}, Threading.SAME_THREAD);
peer.startBlockChainDownload();
control.verify();
List<Sha256Hash> expectedLocator = new ArrayList<Sha256Hash>();
expectedLocator.add(b2.getHash());
expectedLocator.add(b1.getHash());
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
GetBlocksMessage message = (GetBlocksMessage) event.getValue().getMessage();
GetBlocksMessage message = (GetBlocksMessage) outbound(writeTarget);
assertEquals(message.getLocator(), expectedLocator);
assertEquals(Sha256Hash.ZERO_HASH, message.getStopHash());
}
@Test
public void getBlock() throws Exception {
control.replay();
connect();
Block b1 = createFakeBlock(blockStore).block;
@@ -400,19 +395,42 @@ public class PeerTest extends TestWithNetworkConnections {
Future<Block> resultFuture = peer.getBlock(b3.getHash());
assertFalse(resultFuture.isDone());
// Peer asks for it.
GetDataMessage message = (GetDataMessage) event.getValue().getMessage();
GetDataMessage message = (GetDataMessage) outbound(writeTarget);
assertEquals(message.getItems().get(0).hash, b3.getHash());
assertFalse(resultFuture.isDone());
// Peer receives it.
inbound(peer, b3);
inbound(writeTarget, b3);
Block b = resultFuture.get();
assertEquals(b, b3);
}
@Test
public void getLargeBlock() throws Exception {
connect();
Block b1 = createFakeBlock(blockStore).block;
blockChain.add(b1);
Block b2 = makeSolvedTestBlock(b1);
Transaction t = new Transaction(unitTestParams);
t.addInput(b1.getTransactions().get(0).getOutput(0));
t.addOutput(new TransactionOutput(unitTestParams, t, BigInteger.ZERO, new byte[Block.MAX_BLOCK_SIZE - 1000]));
b2.addTransaction(t);
// Request the block.
Future<Block> resultFuture = peer.getBlock(b2.getHash());
assertFalse(resultFuture.isDone());
// Peer asks for it.
GetDataMessage message = (GetDataMessage) outbound(writeTarget);
assertEquals(message.getItems().get(0).hash, b2.getHash());
assertFalse(resultFuture.isDone());
// Peer receives it.
inbound(writeTarget, b2);
Block b = resultFuture.get();
assertEquals(b, b2);
}
@Test
public void fastCatchup() throws Exception {
control.replay();
connect();
// Check that blocks before the fast catchup point are retrieved using getheaders, and after using getblocks.
@@ -429,7 +447,7 @@ public class PeerTest extends TestWithNetworkConnections {
// Request headers until the last 2 blocks.
peer.setDownloadParameters((Utils.now().getTime() / 1000) - (600*2) + 1, false);
peer.startBlockChainDownload();
GetHeadersMessage getheaders = (GetHeadersMessage) outbound();
GetHeadersMessage getheaders = (GetHeadersMessage) outbound(writeTarget);
List<Sha256Hash> expectedLocator = new ArrayList<Sha256Hash>();
expectedLocator.add(b1.getHash());
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
@@ -443,36 +461,38 @@ public class PeerTest extends TestWithNetworkConnections {
expectedLocator.add(b2.getHash());
expectedLocator.add(b1.getHash());
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
inbound(peer, headers);
GetBlocksMessage getblocks = (GetBlocksMessage) outbound();
inbound(writeTarget, headers);
GetBlocksMessage getblocks = (GetBlocksMessage) outbound(writeTarget);
assertEquals(expectedLocator, getblocks.getLocator());
assertEquals(Sha256Hash.ZERO_HASH, getblocks.getStopHash());
// We're supposed to get an inv here.
InventoryMessage inv = new InventoryMessage(unitTestParams);
inv.addItem(new InventoryItem(InventoryItem.Type.Block, b3.getHash()));
inbound(peer, inv);
GetDataMessage getdata = (GetDataMessage) event.getValue().getMessage();
inbound(writeTarget, inv);
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(b3.getHash(), getdata.getItems().get(0).hash);
// All done.
inbound(peer, b3);
inbound(writeTarget, b3);
pingAndWait(writeTarget);
closePeer(peer);
}
@Test
public void pingPong() throws Exception {
control.replay();
connect();
Utils.rollMockClock(0);
// No ping pong happened yet.
assertEquals(Long.MAX_VALUE, peer.getLastPingTime());
assertEquals(Long.MAX_VALUE, peer.getPingTime());
ListenableFuture<Long> future = peer.ping();
Ping pingMsg = (Ping) outbound();
assertEquals(Long.MAX_VALUE, peer.getLastPingTime());
assertEquals(Long.MAX_VALUE, peer.getPingTime());
assertFalse(future.isDone());
Ping pingMsg = (Ping) outbound(writeTarget);
Utils.rollMockClock(5);
// The pong is returned.
inbound(peer, new Pong(pingMsg.getNonce()));
inbound(writeTarget, new Pong(pingMsg.getNonce()));
pingAndWait(writeTarget);
assertTrue(future.isDone());
long elapsed = future.get();
assertTrue("" + elapsed, elapsed > 1000);
@@ -480,9 +500,9 @@ public class PeerTest extends TestWithNetworkConnections {
assertEquals(elapsed, peer.getPingTime());
// Do it again and make sure it affects the average.
future = peer.ping();
pingMsg = (Ping) outbound();
pingMsg = (Ping) outbound(writeTarget);
Utils.rollMockClock(50);
inbound(peer, new Pong(pingMsg.getNonce()));
inbound(writeTarget, new Pong(pingMsg.getNonce()));
elapsed = future.get();
assertEquals(elapsed, peer.getLastPingTime());
assertEquals(7250, peer.getPingTime());
@@ -500,7 +520,6 @@ public class PeerTest extends TestWithNetworkConnections {
public void recursiveDownload(boolean useNotFound) throws Exception {
// Using ping or notfound?
control.replay();
connectWithVersion(useNotFound ? 70001 : 60001);
// Check that we can download all dependencies of an unconfirmed relevant transaction from the mempool.
ECKey to = new ECKey();
@@ -543,16 +562,18 @@ public class PeerTest extends TestWithNetworkConnections {
// Announce the first one. Wait for it to be downloaded.
InventoryMessage inv = new InventoryMessage(unitTestParams);
inv.addTransaction(t1);
inbound(peer, inv);
GetDataMessage getdata = (GetDataMessage) outbound();
inbound(writeTarget, inv);
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
Threading.waitForUserCode();
assertEquals(t1.getHash(), getdata.getItems().get(0).hash);
inbound(peer, t1);
inbound(writeTarget, t1);
pingAndWait(writeTarget);
assertEquals(t1, onTx[0]);
// We want its dependencies so ask for them.
ListenableFuture<List<Transaction>> futures = peer.downloadDependencies(t1);
assertFalse(futures.isDone());
// It will recursively ask for the dependencies of t1: t2, t3, someHash and anotherHash.
getdata = (GetDataMessage) outbound();
getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(4, getdata.getItems().size());
assertEquals(t2.getHash(), getdata.getItems().get(0).hash);
assertEquals(t3.getHash(), getdata.getItems().get(1).hash);
@@ -560,45 +581,46 @@ public class PeerTest extends TestWithNetworkConnections {
assertEquals(anotherHash, getdata.getItems().get(3).hash);
long nonce = -1;
if (!useNotFound)
nonce = ((Ping) outbound()).getNonce();
nonce = ((Ping) outbound(writeTarget)).getNonce();
// For some random reason, t4 is delivered at this point before it's needed - perhaps it was a Bloom filter
// false positive. We do this to check that the mempool is being checked for seen transactions before
// requesting them.
inbound(peer, t4);
inbound(writeTarget, t4);
// Deliver the requested transactions.
inbound(peer, t2);
inbound(peer, t3);
inbound(writeTarget, t2);
inbound(writeTarget, t3);
if (useNotFound) {
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, someHash));
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, anotherHash));
inbound(peer, notFound);
inbound(writeTarget, notFound);
} else {
inbound(peer, new Pong(nonce));
inbound(writeTarget, new Pong(nonce));
}
assertFalse(futures.isDone());
// It will recursively ask for the dependencies of t2: t5 and t4, but not t3 because it already found t4.
getdata = (GetDataMessage) outbound();
getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(getdata.getItems().get(0).hash, t2.getInput(0).getOutpoint().getHash());
// t5 isn't found and t4 is.
if (useNotFound) {
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t5));
inbound(peer, notFound);
inbound(writeTarget, notFound);
} else {
bouncePing();
}
assertFalse(futures.isDone());
// Continue to explore the t4 branch and ask for t6, which is in the chain.
getdata = (GetDataMessage) outbound();
getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(t6, getdata.getItems().get(0).hash);
if (useNotFound) {
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t6));
inbound(peer, notFound);
inbound(writeTarget, notFound);
} else {
bouncePing();
}
pingAndWait(writeTarget);
// That's it, we explored the entire tree.
assertTrue(futures.isDone());
List<Transaction> results = futures.get();
@@ -608,8 +630,8 @@ public class PeerTest extends TestWithNetworkConnections {
}
private void bouncePing() throws Exception {
Ping ping = (Ping) outbound();
inbound(peer, new Pong(ping.getNonce()));
Ping ping = (Ping) outbound(writeTarget);
inbound(writeTarget, new Pong(ping.getNonce()));
}
@Test
@@ -623,7 +645,6 @@ public class PeerTest extends TestWithNetworkConnections {
}
public void timeLockedTransaction(boolean useNotFound) throws Exception {
control.replay();
connectWithVersion(useNotFound ? 70001 : 60001);
// Test that if we receive a relevant transaction that has a lock time, it doesn't result in a notification
// until we explicitly opt in to seeing those.
@@ -640,31 +661,33 @@ public class PeerTest extends TestWithNetworkConnections {
});
// Send a normal relevant transaction, it's received correctly.
Transaction t1 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), key);
inbound(peer, t1);
GetDataMessage getdata = (GetDataMessage) outbound();
inbound(writeTarget, t1);
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
if (useNotFound) {
inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
inbound(writeTarget, new NotFoundMessage(unitTestParams, getdata.getItems()));
} else {
bouncePing();
}
pingAndWait(writeTarget);
Threading.waitForUserCode();
assertNotNull(vtx[0]);
vtx[0] = null;
// Send a timelocked transaction, nothing happens.
Transaction t2 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(2, 0), key);
t2.setLockTime(999999);
inbound(peer, t2);
inbound(writeTarget, t2);
Threading.waitForUserCode();
assertNull(vtx[0]);
// Now we want to hear about them. Send another, we are told about it.
wallet.setAcceptRiskyTransactions(true);
inbound(peer, t2);
getdata = (GetDataMessage) outbound();
inbound(writeTarget, t2);
getdata = (GetDataMessage) outbound(writeTarget);
if (useNotFound) {
inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
inbound(writeTarget, new NotFoundMessage(unitTestParams, getdata.getItems()));
} else {
bouncePing();
}
pingAndWait(writeTarget);
Threading.waitForUserCode();
assertEquals(t2, vtx[0]);
}
@@ -697,7 +720,6 @@ public class PeerTest extends TestWithNetworkConnections {
private void checkTimeLockedDependency(boolean shouldAccept, boolean useNotFound) throws Exception {
// Initial setup.
control.replay();
connectWithVersion(useNotFound ? 70001 : 60001);
ECKey key = new ECKey();
Wallet wallet = new Wallet(unitTestParams);
@@ -725,30 +747,31 @@ public class PeerTest extends TestWithNetworkConnections {
// Announce t1.
InventoryMessage inv = new InventoryMessage(unitTestParams);
inv.addTransaction(t1);
inbound(peer, inv);
inbound(writeTarget, inv);
// Send it.
GetDataMessage getdata = (GetDataMessage) outbound();
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(t1.getHash(), getdata.getItems().get(0).hash);
inbound(peer, t1);
inbound(writeTarget, t1);
// Nothing arrived at our event listener yet.
assertNull(vtx[0]);
// We request t2.
getdata = (GetDataMessage) outbound();
getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(t2.getHash(), getdata.getItems().get(0).hash);
inbound(peer, t2);
inbound(writeTarget, t2);
if (!useNotFound)
bouncePing();
// We request t3.
getdata = (GetDataMessage) outbound();
getdata = (GetDataMessage) outbound(writeTarget);
assertEquals(t3, getdata.getItems().get(0).hash);
// Can't find it: bottom of tree.
if (useNotFound) {
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t3));
inbound(peer, notFound);
inbound(writeTarget, notFound);
} else {
bouncePing();
}
pingAndWait(writeTarget);
Threading.waitForUserCode();
// We're done but still not notified because it was timelocked.
if (shouldAccept)
@@ -759,29 +782,51 @@ public class PeerTest extends TestWithNetworkConnections {
@Test
public void disconnectOldVersions1() throws Exception {
expect(channel.close()).andReturn(null);
control.replay();
// Set up the connection with an old version.
handler.connectRequested(ctx, new UpstreamChannelStateEvent(channel, ChannelState.CONNECTED, socketAddress));
VersionMessage peerVersion = new VersionMessage(unitTestParams, OTHER_PEER_CHAIN_HEIGHT);
peerVersion.clientVersion = 500;
DownstreamMessageEvent versionEvent =
new DownstreamMessageEvent(channel, Channels.future(channel), peerVersion, null);
handler.messageReceived(ctx, versionEvent);
final SettableFuture<Void> connectedFuture = SettableFuture.create();
final SettableFuture<Void> disconnectedFuture = SettableFuture.create();
peer.addEventListener(new AbstractPeerEventListener() {
@Override
public void onPeerConnected(Peer peer, int peerCount) {
connectedFuture.set(null);
}
@Override
public void onPeerDisconnected(Peer peer, int peerCount) {
disconnectedFuture.set(null);
}
});
connectWithVersion(500);
connectedFuture.get();
disconnectedFuture.get();
try {
peer.writeTarget.writeBytes(new byte[1]);
fail();
} catch (IOException e) {
assertTrue(e instanceof ClosedChannelException ||
(e instanceof SocketException && e.getMessage().equals("Socket is closed")));
}
}
@Test
public void disconnectOldVersions2() throws Exception {
expect(channel.close()).andReturn(null);
control.replay();
// Set up the connection with an old version.
handler.connectRequested(ctx, new UpstreamChannelStateEvent(channel, ChannelState.CONNECTED, socketAddress));
VersionMessage peerVersion = new VersionMessage(unitTestParams, OTHER_PEER_CHAIN_HEIGHT);
peerVersion.clientVersion = 70000;
DownstreamMessageEvent versionEvent =
new DownstreamMessageEvent(channel, Channels.future(channel), peerVersion, null);
handler.messageReceived(ctx, versionEvent);
final SettableFuture<Void> connectedFuture = SettableFuture.create();
final SettableFuture<Void> disconnectedFuture = SettableFuture.create();
peer.addEventListener(new AbstractPeerEventListener() {
@Override
public void onPeerConnected(Peer peer, int peerCount) {
connectedFuture.set(null);
}
@Override
public void onPeerDisconnected(Peer peer, int peerCount) {
disconnectedFuture.set(null);
}
});
peer.setMinProtocolVersion(500);
connectWithVersion(542);
pingAndWait(writeTarget);
}
@Test
@@ -808,7 +853,6 @@ public class PeerTest extends TestWithNetworkConnections {
throw new RuntimeException();
}
});
control.replay();
connect();
Transaction t1 = new Transaction(unitTestParams);
t1.addInput(new TransactionInput(unitTestParams, t1, new byte[]{}));
@@ -816,10 +860,11 @@ public class PeerTest extends TestWithNetworkConnections {
Transaction t2 = new Transaction(unitTestParams);
t2.addInput(t1.getOutput(0));
t2.addOutput(Utils.toNanoCoins(1, 0), wallet.getChangeAddress());
inbound(peer, t2);
inbound(writeTarget, t2);
final InventoryItem inventoryItem = new InventoryItem(InventoryItem.Type.Transaction, t2.getInput(0).getOutpoint().getHash());
final NotFoundMessage nfm = new NotFoundMessage(unitTestParams, Lists.newArrayList(inventoryItem));
inbound(peer, nfm);
inbound(writeTarget, nfm);
pingAndWait(writeTarget);
Threading.waitForUserCode();
assertTrue(throwables[0] instanceof NullPointerException);
Threading.uncaughtExceptionHandler = null;
@@ -835,19 +880,11 @@ public class PeerTest extends TestWithNetworkConnections {
result.setException(throwable);
}
};
ServerSocket server = new ServerSocket(0);
final NetworkParameters params = TestNet3Params.get();
Peer peer = new Peer(params, blockChain, "test", "1.0");
ListenableFuture<TCPNetworkConnection> future = TCPNetworkConnection.connectTo(TestNet3Params.get(),
new InetSocketAddress(InetAddress.getLocalHost(), server.getLocalPort()), 5000, peer);
Socket socket = server.accept();
// Write out a verack+version.
connect(); // Writes out a verack+version.
final NetworkParameters params = TestNet3Params.testNet();
BitcoinSerializer serializer = new BitcoinSerializer(params);
final VersionMessage ver = new VersionMessage(params, 1000);
ver.localServices = VersionMessage.NODE_NETWORK;
serializer.serialize(ver, socket.getOutputStream());
serializer.serialize(new VersionAck(), socket.getOutputStream());
// Now write some bogus truncated message.
ByteArrayOutputStream out = new ByteArrayOutputStream();
serializer.serialize("inv", new InventoryMessage(params) {
@Override
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
@@ -863,22 +900,20 @@ public class PeerTest extends TestWithNetworkConnections {
bits = Arrays.copyOf(bits, bits.length / 2);
stream.write(bits);
}
}.bitcoinSerialize(), socket.getOutputStream());
}.bitcoinSerialize(), out);
writeTarget.writeTarget.writeBytes(out.toByteArray());
try {
result.get();
fail();
} catch (ExecutionException e) {
assertTrue(e.getCause() instanceof ProtocolException);
}
}
// TODO: Use generics here to avoid unnecessary casting.
private Message outbound() {
List<DownstreamMessageEvent> messages = event.getValues();
if (messages.isEmpty())
throw new AssertionError("No messages sent when one was expected");
Message message = (Message)messages.get(0).getMessage();
messages.remove(0);
return message;
try {
peer.writeTarget.writeBytes(new byte[1]);
fail();
} catch (IOException e) {
assertTrue(e instanceof ClosedChannelException ||
(e instanceof SocketException && e.getMessage().equals("Socket is closed")));
}
}
}

View File

@@ -16,40 +16,58 @@
package com.google.bitcoin.core;
import com.google.bitcoin.networkabstraction.*;
import com.google.bitcoin.params.UnitTestParams;
import com.google.bitcoin.store.BlockStore;
import com.google.bitcoin.store.MemoryBlockStore;
import com.google.bitcoin.utils.BriefLogFormatter;
import org.easymock.EasyMock;
import org.easymock.IMocksControl;
import org.jboss.netty.channel.*;
import com.google.bitcoin.utils.Threading;
import com.google.common.util.concurrent.SettableFuture;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javax.annotation.Nullable;
import static org.easymock.EasyMock.createStrictControl;
import static org.easymock.EasyMock.expect;
import static com.google.common.base.Preconditions.checkArgument;
import static org.junit.Assert.assertTrue;
/**
* Utility class that makes it easy to work with mock NetworkConnections.
*/
public class TestWithNetworkConnections {
protected IMocksControl control;
protected NetworkParameters unitTestParams;
protected BlockStore blockStore;
protected BlockChain blockChain;
protected Wallet wallet;
protected ECKey key;
protected Address address;
private static int fakePort;
protected ChannelHandlerContext ctx;
protected Channel channel;
protected SocketAddress socketAddress;
protected ChannelPipeline pipeline;
private NioServer peerServer;
private final ClientConnectionManager channels;
protected final BlockingQueue<InboundMessageQueuer> newPeerWriteTargetQueue = new LinkedBlockingQueue<InboundMessageQueuer>();
enum ClientType {
NIO_CLIENT_MANAGER,
BLOCKING_CLIENT_MANAGER,
NIO_CLIENT,
BLOCKING_CLIENT
}
private final ClientType clientType;
public TestWithNetworkConnections(ClientType clientType) {
this.clientType = clientType;
if (clientType == ClientType.NIO_CLIENT_MANAGER)
channels = new NioClientManager();
else if (clientType == ClientType.BLOCKING_CLIENT_MANAGER)
channels = new BlockingClientManager();
else
channels = null;
}
public void setUp() throws Exception {
setUp(new MemoryBlockStore(UnitTestParams.get()));
}
@@ -57,9 +75,6 @@ public class TestWithNetworkConnections {
public void setUp(BlockStore blockStore) throws Exception {
BriefLogFormatter.init();
control = createStrictControl();
control.checkOrder(false);
unitTestParams = UnitTestParams.get();
Wallet.SendRequest.DEFAULT_FEE_PER_KB = BigInteger.ZERO;
this.blockStore = blockStore;
@@ -69,77 +84,106 @@ public class TestWithNetworkConnections {
wallet.addKey(key);
blockChain = new BlockChain(unitTestParams, wallet, blockStore);
socketAddress = new InetSocketAddress("127.0.0.1", 1111);
peerServer = new NioServer(new StreamParserFactory() {
@Nullable
@Override
public StreamParser getNewParser(InetAddress inetAddress, int port) {
return new InboundMessageQueuer(unitTestParams) {
@Override public void connectionClosed() { }
@Override
public void connectionOpened() {
newPeerWriteTargetQueue.offer(this);
}
};
}
}, new InetSocketAddress("127.0.0.1", 2000));
peerServer.startAndWait();
if (clientType == ClientType.NIO_CLIENT_MANAGER || clientType == ClientType.BLOCKING_CLIENT_MANAGER)
channels.startAndWait();
ctx = createChannelHandlerContext();
channel = createChannel();
pipeline = createPipeline(channel);
socketAddress = new InetSocketAddress("127.0.0.1", 1111);
}
public void tearDown() throws Exception {
Wallet.SendRequest.DEFAULT_FEE_PER_KB = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE;
peerServer.stopAndWait();
}
protected ChannelPipeline createPipeline(Channel channel) {
ChannelPipeline pipeline = control.createMock(ChannelPipeline.class);
expect(channel.getPipeline()).andStubReturn(pipeline);
return pipeline;
}
protected Channel createChannel() {
Channel channel = control.createMock(Channel.class);
expect(channel.getRemoteAddress()).andStubReturn(socketAddress);
return channel;
}
protected ChannelHandlerContext createChannelHandlerContext() {
ChannelHandlerContext ctx1 = control.createMock(ChannelHandlerContext.class);
ctx1.sendDownstream(EasyMock.anyObject(ChannelEvent.class));
EasyMock.expectLastCall().anyTimes();
ctx1.sendUpstream(EasyMock.anyObject(ChannelEvent.class));
EasyMock.expectLastCall().anyTimes();
return ctx1;
}
protected MockNetworkConnection createMockNetworkConnection() {
MockNetworkConnection conn = new MockNetworkConnection();
try {
conn.connect(new PeerAddress(InetAddress.getLocalHost(), fakePort++), 0);
} catch (UnknownHostException e) {
throw new RuntimeException(e); // Cannot happen
}
return conn;
protected InboundMessageQueuer connect(Peer peer, VersionMessage versionMessage) throws Exception {
checkArgument(versionMessage.hasBlockChain());
if (clientType == ClientType.NIO_CLIENT_MANAGER || clientType == ClientType.BLOCKING_CLIENT_MANAGER)
channels.openConnection(new InetSocketAddress("127.0.0.1", 2000), peer);
else if (clientType == ClientType.NIO_CLIENT)
new NioClient(new InetSocketAddress("127.0.0.1", 2000), peer, 100);
else if (clientType == ClientType.BLOCKING_CLIENT)
new BlockingClient(new InetSocketAddress("127.0.0.1", 2000), peer, 100, null);
else
throw new RuntimeException();
// Claim we are connected to a different IP that what we really are, so tx confidence broadcastBy sets work
InboundMessageQueuer writeTarget = newPeerWriteTargetQueue.take();
writeTarget.peer = peer;
// Complete handshake with the peer - send/receive version(ack)s, receive bloom filter
writeTarget.sendMessage(versionMessage);
writeTarget.sendMessage(new VersionAck());
assertTrue(writeTarget.nextMessageBlocking() instanceof VersionMessage);
assertTrue(writeTarget.nextMessageBlocking() instanceof VersionAck);
return writeTarget;
}
protected void closePeer(Peer peer) throws Exception {
peer.getHandler().channelClosed(ctx,
new UpstreamChannelStateEvent(channel, ChannelState.CONNECTED, null));
}
protected void inbound(Peer peer, Message message) throws Exception {
peer.getHandler().messageReceived(ctx,
new UpstreamMessageEvent(channel, message, socketAddress));
peer.close();
}
protected void inbound(FakeChannel peerChannel, Message message) {
Channels.fireMessageReceived(peerChannel, message);
protected void inbound(InboundMessageQueuer peerChannel, Message message) {
peerChannel.sendMessage(message);
}
protected Object outbound(FakeChannel p1) {
ChannelEvent channelEvent = p1.nextEvent();
if (channelEvent != null && !(channelEvent instanceof MessageEvent))
throw new IllegalStateException("Expected message but got: " + channelEvent);
MessageEvent nextEvent = (MessageEvent) channelEvent;
if (nextEvent == null)
return null;
return nextEvent.getMessage();
private void outboundPingAndWait(final InboundMessageQueuer p, long nonce) throws Exception {
// Send a ping and wait for it to get to the other side
SettableFuture<Void> pingReceivedFuture = SettableFuture.create();
p.mapPingFutures.put(nonce, pingReceivedFuture);
p.peer.sendMessage(new Ping(nonce));
pingReceivedFuture.get();
p.mapPingFutures.remove(nonce);
}
protected Object waitForOutbound(FakeChannel ch) throws InterruptedException {
return ((MessageEvent)ch.nextEventBlocking()).getMessage();
private void inboundPongAndWait(final InboundMessageQueuer p, final long nonce) throws Exception {
// Receive a ping (that the Peer doesn't see) and wait for it to get through the socket
final SettableFuture<Void> pongReceivedFuture = SettableFuture.create();
PeerEventListener listener = new AbstractPeerEventListener() {
@Override
public Message onPreMessageReceived(Peer p, Message m) {
if (m instanceof Pong && ((Pong) m).getNonce() == nonce) {
pongReceivedFuture.set(null);
return null;
}
return m;
}
};
p.peer.addEventListener(listener, Threading.SAME_THREAD);
inbound(p, new Pong(nonce));
pongReceivedFuture.get();
p.peer.removeEventListener(listener);
}
protected Peer peerOf(Channel ch) {
return PeerGroup.peerFromChannel(ch);
protected void pingAndWait(final InboundMessageQueuer p) throws Exception {
final long nonce = (long) (Math.random() * Long.MAX_VALUE);
// Start with an inbound Pong as pingAndWait often happens immediately after an inbound() call, and then wants
// to wait on an outbound message, so we do it in the same order or we see race conditions
inboundPongAndWait(p, nonce);
outboundPingAndWait(p, nonce);
}
protected Message outbound(InboundMessageQueuer p1) throws Exception {
pingAndWait(p1);
return p1.nextMessage();
}
protected Object waitForOutbound(InboundMessageQueuer ch) throws InterruptedException {
return ch.nextMessageBlocking();
}
protected Peer peerOf(InboundMessageQueuer ch) {
return ch.peer;
}
}

View File

@@ -17,12 +17,13 @@
package com.google.bitcoin.core;
import com.google.bitcoin.params.UnitTestParams;
import com.google.bitcoin.networkabstraction.BlockingClientManager;
import com.google.bitcoin.networkabstraction.NioClientManager;
import com.google.bitcoin.store.BlockStore;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.*;
import java.net.InetSocketAddress;
import static com.google.common.base.Preconditions.checkArgument;
import static org.junit.Assert.assertTrue;
/**
@@ -33,55 +34,58 @@ public class TestWithPeerGroup extends TestWithNetworkConnections {
protected PeerGroup peerGroup;
protected VersionMessage remoteVersionMessage;
private ClientBootstrap bootstrap;
private final ClientType clientType;
public TestWithPeerGroup(ClientType clientType) {
super(clientType);
if (clientType != ClientType.NIO_CLIENT_MANAGER && clientType != ClientType.BLOCKING_CLIENT_MANAGER)
throw new RuntimeException();
this.clientType = clientType;
}
public void setUp(BlockStore blockStore) throws Exception {
super.setUp(blockStore);
remoteVersionMessage = new VersionMessage(unitTestParams, 1);
remoteVersionMessage.localServices = VersionMessage.NODE_NETWORK;
remoteVersionMessage.clientVersion = FilteredBlock.MIN_PROTOCOL_VERSION;
initPeerGroup();
}
protected void initPeerGroup() {
bootstrap = new ClientBootstrap(new ChannelFactory() {
public void releaseExternalResources() {}
public Channel newChannel(ChannelPipeline pipeline) {
ChannelSink sink = new FakeChannelSink();
return new FakeChannel(this, pipeline, sink);
}
public void shutdown() {}
});
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
VersionMessage ver = new VersionMessage(unitTestParams, 1);
ChannelPipeline p = Channels.pipeline();
Peer peer = new Peer(unitTestParams, blockChain, ver, peerGroup.getMemoryPool());
peer.addLifecycleListener(peerGroup.startupListener);
p.addLast("peer", peer.getHandler());
return p;
}
});
peerGroup = new PeerGroup(unitTestParams, blockChain, bootstrap);
if (clientType == ClientType.NIO_CLIENT_MANAGER)
peerGroup = new PeerGroup(unitTestParams, blockChain, new NioClientManager());
else
peerGroup = new PeerGroup(unitTestParams, blockChain, new BlockingClientManager());
peerGroup.setPingIntervalMsec(0); // Disable the pings as they just get in the way of most tests.
}
protected FakeChannel connectPeer(int id) {
protected InboundMessageQueuer connectPeerWithoutVersionExchange(int id) throws Exception {
InetSocketAddress remoteAddress = new InetSocketAddress("127.0.0.1", 2000);
Peer peer = peerGroup.connectTo(remoteAddress).getConnectionOpenFuture().get();
// Claim we are connected to a different IP that what we really are, so tx confidence broadcastBy sets work
peer.remoteIp = new InetSocketAddress("127.0.0.1", 2000 + id);
InboundMessageQueuer writeTarget = newPeerWriteTargetQueue.take();
writeTarget.peer = peer;
return writeTarget;
}
protected InboundMessageQueuer connectPeer(int id) throws Exception {
return connectPeer(id, remoteVersionMessage);
}
protected FakeChannel connectPeer(int id, VersionMessage versionMessage) {
InetSocketAddress remoteAddress = new InetSocketAddress("127.0.0.1", 2000 + id);
FakeChannel p = (FakeChannel) peerGroup.connectTo(remoteAddress).getChannel();
assertTrue(p.nextEvent() instanceof ChannelStateEvent);
inbound(p, versionMessage);
inbound(p, new VersionAck());
protected InboundMessageQueuer connectPeer(int id, VersionMessage versionMessage) throws Exception {
checkArgument(versionMessage.hasBlockChain());
InboundMessageQueuer writeTarget = connectPeerWithoutVersionExchange(id);
// Complete handshake with the peer - send/receive version(ack)s, receive bloom filter
writeTarget.sendMessage(versionMessage);
writeTarget.sendMessage(new VersionAck());
assertTrue(writeTarget.nextMessageBlocking() instanceof VersionMessage);
assertTrue(writeTarget.nextMessageBlocking() instanceof VersionAck);
if (versionMessage.isBloomFilteringSupported()) {
assertTrue(outbound(p) instanceof BloomFilter);
assertTrue(outbound(p) instanceof MemoryPoolMessage);
assertTrue(writeTarget.nextMessageBlocking() instanceof BloomFilter);
assertTrue(writeTarget.nextMessageBlocking() instanceof MemoryPoolMessage);
}
return p;
return writeTarget;
}
}

View File

@@ -21,16 +21,34 @@ import com.google.bitcoin.store.MemoryBlockStore;
import com.google.bitcoin.utils.TestUtils;
import com.google.bitcoin.utils.Threading;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collection;
import java.util.Random;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
@RunWith(value = Parameterized.class)
public class TransactionBroadcastTest extends TestWithPeerGroup {
static final NetworkParameters params = UnitTestParams.get();
@Parameterized.Parameters
public static Collection<ClientType[]> parameters() {
return Arrays.asList(new ClientType[] {ClientType.NIO_CLIENT_MANAGER},
new ClientType[] {ClientType.BLOCKING_CLIENT_MANAGER});
}
public TransactionBroadcastTest(ClientType clientType) {
super(clientType);
}
@Override
@Before
public void setUp() throws Exception {
@@ -39,11 +57,18 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
// Fix the random permutation that TransactionBroadcast uses to shuffle the peers.
TransactionBroadcast.random = new Random(0);
peerGroup.setMinBroadcastConnections(2);
peerGroup.startAndWait();
}
@After
public void tearDown() throws Exception {
super.tearDown();
peerGroup.stopAndWait();
}
@Test
public void fourPeers() throws Exception {
FakeChannel[] channels = { connectPeer(1), connectPeer(2), connectPeer(3), connectPeer(4) };
InboundMessageQueuer[] channels = { connectPeer(1), connectPeer(2), connectPeer(3), connectPeer(4) };
Transaction tx = new Transaction(params);
TransactionBroadcast broadcast = new TransactionBroadcast(peerGroup, tx);
ListenableFuture<Transaction> future = broadcast.broadcast();
@@ -64,6 +89,7 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
Threading.waitForUserCode();
assertFalse(future.isDone());
inbound(channels[1], InventoryMessage.with(tx));
pingAndWait(channels[1]);
Threading.waitForUserCode();
assertTrue(future.isDone());
}
@@ -72,7 +98,7 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
public void retryFailedBroadcast() throws Exception {
// If we create a spend, it's sent to a peer that swallows it, and the peergroup is removed/re-added then
// the tx should be broadcast again.
FakeChannel p1 = connectPeer(1, new VersionMessage(params, 2));
InboundMessageQueuer p1 = connectPeer(1);
connectPeer(2);
// Send ourselves a bit of money.
@@ -90,11 +116,7 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
// p1 eats it :( A bit later the PeerGroup is taken down.
peerGroup.removeWallet(wallet);
// ... and put back.
initPeerGroup();
peerGroup.addWallet(wallet);
p1 = connectPeer(1, new VersionMessage(params, 2));
connectPeer(2);
// We want to hear about it again. Now, because we've disabled the randomness for the unit tests it will
// re-appear on p1 again. Of course in the real world it would end up with a different set of peers and
@@ -108,12 +130,15 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
// Make sure we can create spends, and that they are announced. Then do the same with offline mode.
// Set up connections and block chain.
FakeChannel p1 = connectPeer(1, new VersionMessage(params, 2));
FakeChannel p2 = connectPeer(2);
VersionMessage ver = new VersionMessage(params, 2);
ver.localServices = VersionMessage.NODE_NETWORK;
InboundMessageQueuer p1 = connectPeer(1, ver);
InboundMessageQueuer p2 = connectPeer(2);
// Send ourselves a bit of money.
Block b1 = TestUtils.makeSolvedTestBlock(blockStore, address);
inbound(p1, b1);
pingAndWait(p1);
assertNull(outbound(p1));
assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());
@@ -143,6 +168,7 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
InventoryMessage inv = new InventoryMessage(params);
inv.addTransaction(t1);
inbound(p2, inv);
pingAndWait(p2);
Threading.waitForUserCode();
assertTrue(sendResult.broadcastComplete.isDone());
assertEquals(transactions[0], sendResult.tx);
@@ -150,6 +176,7 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
// Confirm it.
Block b2 = TestUtils.createFakeBlock(blockStore, t1).block;
inbound(p1, b2);
pingAndWait(p1);
assertNull(outbound(p1));
// Do the same thing with an offline transaction.

View File

@@ -14,11 +14,14 @@
* limitations under the License.
*/
package com.google.bitcoin.protocols.niowrapper;
package com.google.bitcoin.networkabstraction;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.bitcoin.core.Utils;
@@ -28,12 +31,48 @@ import org.bitcoin.paymentchannel.Protos;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static com.google.common.base.Preconditions.checkState;
import static org.junit.Assert.*;
public class NioWrapperTest {
@RunWith(value = Parameterized.class)
public class NetworkAbstractionTests {
private AtomicBoolean fail;
private final int clientType;
private final ClientConnectionManager channels;
@Parameterized.Parameters
public static Collection<Integer[]> parameters() {
return Arrays.asList(new Integer[]{0}, new Integer[]{1}, new Integer[]{2}, new Integer[]{3});
}
public NetworkAbstractionTests(Integer clientType) throws Exception {
this.clientType = clientType;
if (clientType == 0) {
channels = new NioClientManager();
channels.start();
} else if (clientType == 1) {
channels = new BlockingClientManager();
channels.start();
} else
channels = null;
}
private MessageWriteTarget openConnection(SocketAddress addr, ProtobufParser parser) throws Exception {
if (clientType == 0 || clientType == 1) {
channels.openConnection(addr, parser);
if (parser.writeTarget.get() == null)
Thread.sleep(100);
return (MessageWriteTarget) parser.writeTarget.get();
} else if (clientType == 2)
return new NioClient(addr, parser, 100);
else if (clientType == 3)
return new BlockingClient(addr, parser, 100, null);
else
throw new RuntimeException();
}
@Before
public void setUp() {
@@ -76,8 +115,8 @@ public class NioWrapperTest {
}
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
}
});
server.start(new InetSocketAddress("localhost", 4243));
}, new InetSocketAddress("localhost", 4243));
server.startAndWait();
ProtobufParser<Protos.TwoWayChannelMessage> clientHandler = new ProtobufParser<Protos.TwoWayChannelMessage>(
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
@@ -100,7 +139,7 @@ public class NioWrapperTest {
}
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
NioClient client = new NioClient(new InetSocketAddress("localhost", 4243), clientHandler, 0);
MessageWriteTarget client = openConnection(new InetSocketAddress("localhost", 4243), clientHandler);
clientConnectionOpen.get();
serverConnectionOpen.get();
@@ -114,7 +153,8 @@ public class NioWrapperTest {
serverConnectionClosed.get();
clientConnectionClosed.get();
server.stop();
server.stopAndWait();
assertFalse(server.isRunning());
}
@Test
@@ -157,10 +197,10 @@ public class NioWrapperTest {
}
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 10);
}
});
server.start(new InetSocketAddress("localhost", 4243));
}, new InetSocketAddress("localhost", 4243));
server.startAndWait();
new NioClient(new InetSocketAddress("localhost", 4243), new ProtobufParser<Protos.TwoWayChannelMessage>(
openConnection(new InetSocketAddress("localhost", 4243), new ProtobufParser<Protos.TwoWayChannelMessage>(
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
@Override
public void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) {
@@ -176,7 +216,7 @@ public class NioWrapperTest {
public void connectionClosed(ProtobufParser handler) {
clientConnection1Closed.set(null);
}
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0), 0);
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0));
clientConnection1Open.get();
serverConnection1Open.get();
@@ -202,7 +242,7 @@ public class NioWrapperTest {
clientConnection2Closed.set(null);
}
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
NioClient client2 = new NioClient(new InetSocketAddress("localhost", 4243), client2Handler, 0);
openConnection(new InetSocketAddress("localhost", 4243), client2Handler);
clientConnection2Open.get();
serverConnection2Open.get();
@@ -213,7 +253,7 @@ public class NioWrapperTest {
clientConnection2Closed.get();
serverConnection2Closed.get();
server.stop();
server.stopAndWait();
}
@Test
@@ -247,8 +287,8 @@ public class NioWrapperTest {
}
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 0x10000, 0);
}
});
server.start(new InetSocketAddress("localhost", 4243));
}, new InetSocketAddress("localhost", 4243));
server.startAndWait();
ProtobufParser<Protos.TwoWayChannelMessage> clientHandler = new ProtobufParser<Protos.TwoWayChannelMessage>(
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
@@ -279,7 +319,7 @@ public class NioWrapperTest {
}
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 0x10000, 0);
NioClient client = new NioClient(new InetSocketAddress("localhost", 4243), clientHandler, 0);
MessageWriteTarget client = openConnection(new InetSocketAddress("localhost", 4243), clientHandler);
clientConnectionOpen.get();
serverConnectionOpen.get();
@@ -358,7 +398,7 @@ public class NioWrapperTest {
serverConnectionClosed.get();
clientConnectionClosed.get();
server.stop();
server.stopAndWait();
}
@Test
@@ -411,8 +451,8 @@ public class NioWrapperTest {
}
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
}
});
server.start(new InetSocketAddress("localhost", 4243));
}, new InetSocketAddress("localhost", 4243));
server.startAndWait();
ProtobufParser<Protos.TwoWayChannelMessage> client1Handler = new ProtobufParser<Protos.TwoWayChannelMessage>(
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
@@ -431,7 +471,7 @@ public class NioWrapperTest {
client1ConnectionClosed.set(null);
}
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
NioClient client1 = new NioClient(new InetSocketAddress("localhost", 4243), client1Handler, 0);
MessageWriteTarget client1 = openConnection(new InetSocketAddress("localhost", 4243), client1Handler);
client1ConnectionOpen.get();
serverConnection1Open.get();
@@ -453,7 +493,7 @@ public class NioWrapperTest {
client2ConnectionClosed.set(null);
}
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
NioClient client2 = new NioClient(new InetSocketAddress("localhost", 4243), client2Handler, 0);
openConnection(new InetSocketAddress("localhost", 4243), client2Handler);
client2ConnectionOpen.get();
serverConnection2Open.get();
@@ -497,17 +537,18 @@ public class NioWrapperTest {
client3Handler.write(msg3);
assertEquals(msg3, client3MessageReceived.get());
// Try to create a race condition by triggering handlerTread closing and client3 closing at the same time
// Try to create a race condition by triggering handlerThread closing and client3 closing at the same time
// This often triggers ClosedByInterruptException in handleKey
server.handlerThread.interrupt();
server.stop();
server.selector.wakeup();
client3.closeConnection();
client3ConnectionClosed.get();
serverConnectionClosed3.get();
server.handlerThread.join();
server.stopAndWait();
client2ConnectionClosed.get();
serverConnectionClosed2.get();
server.stop();
server.stopAndWait();
}
}

View File

@@ -16,17 +16,21 @@
package com.google.bitcoin.examples;
import com.google.bitcoin.core.AbstractPeerEventListener;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.TCPNetworkConnection;
import com.google.bitcoin.core.Peer;
import com.google.bitcoin.core.VersionMessage;
import com.google.bitcoin.discovery.DnsDiscovery;
import com.google.bitcoin.discovery.PeerDiscoveryException;
import com.google.bitcoin.networkabstraction.NioClient;
import com.google.bitcoin.networkabstraction.NioClientManager;
import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@@ -74,16 +78,16 @@ public class PrintPeers {
final Object lock = new Object();
final long[] bestHeight = new long[1];
List<ListenableFuture<TCPNetworkConnection>> futures = Lists.newArrayList();
List<ListenableFuture<Void>> futures = Lists.newArrayList();
NioClientManager clientManager = new NioClientManager();
for (final InetAddress addr : addrs) {
final ListenableFuture<TCPNetworkConnection> future =
TCPNetworkConnection.connectTo(params, new InetSocketAddress(addr, params.getPort()), 1000 /* timeout */, null);
futures.add(future);
final Peer peer = new Peer(params, new VersionMessage(params, 0), null, new InetSocketAddress(addr, params.getPort()));
final SettableFuture future = SettableFuture.create();
// Once the connection has completed version handshaking ...
Futures.addCallback(future, new FutureCallback<TCPNetworkConnection>() {
public void onSuccess(TCPNetworkConnection conn) {
peer.addEventListener(new AbstractPeerEventListener() {
public void onPeerConnected(Peer p, int peerCount) {
// Check the chain height it claims to have.
VersionMessage ver = conn.getVersionMessage();
VersionMessage ver = peer.getPeerVersionMessage();
long nodeHeight = ver.bestHeight;
synchronized (lock) {
long diff = bestHeight[0] - nodeHeight;
@@ -97,13 +101,19 @@ public class PrintPeers {
bestHeight[0] = nodeHeight;
}
}
conn.close();
// Now finish the future and close the connection
future.set(null);
peer.close();
}
public void onFailure(Throwable throwable) {
System.out.println("Failed to talk to " + addr + ": " + throwable.getMessage());
public void onPeerDisconnected(Peer p, int peerCount) {
if (!future.isDone())
System.out.println("Failed to talk to " + addr);
future.set(null);
}
});
clientManager.openConnection(new InetSocketAddress(addr, params.getPort()), peer);
futures.add(future);
}
// Wait for every tried connection to finish.
Futures.successfulAsList(futures).get();

View File

@@ -102,13 +102,6 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty</artifactId>
<version>3.6.3.Final</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.madgag</groupId>
<artifactId>sc-light-jdk15on</artifactId>