mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-11-02 05:27:17 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
7
pom.xml
7
pom.xml
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user