mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-11-02 21:47:18 +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>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.netty</groupId>
|
|
||||||
<artifactId>netty</artifactId>
|
|
||||||
<version>3.6.3.Final</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.madgag</groupId>
|
<groupId>com.madgag</groupId>
|
||||||
<artifactId>sc-light-jdk15on</artifactId>
|
<artifactId>sc-light-jdk15on</artifactId>
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
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.
|
// A Bitcoin protocol message has the following format.
|
||||||
//
|
//
|
||||||
// - 4 byte magic number: 0xfabfb5da for the testnet or
|
// - 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
|
* Deserializes only the header in case packet meta data is needed before decoding
|
||||||
* the payload. This method assumes you have already called seekPastMagicBytes()
|
* 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);
|
return new BitcoinPacketHeader(in);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,16 +165,9 @@ public class BitcoinSerializer {
|
|||||||
* Deserialize payload only. You must provide a header, typically obtained by calling
|
* Deserialize payload only. You must provide a header, typically obtained by calling
|
||||||
* {@link BitcoinSerializer#deserializeHeader}.
|
* {@link BitcoinSerializer#deserializeHeader}.
|
||||||
*/
|
*/
|
||||||
public Message deserializePayload(BitcoinPacketHeader header, InputStream in) throws ProtocolException, IOException {
|
public Message deserializePayload(BitcoinPacketHeader header, ByteBuffer in) throws ProtocolException, BufferUnderflowException {
|
||||||
int readCursor = 0;
|
|
||||||
byte[] payloadBytes = new byte[header.size];
|
byte[] payloadBytes = new byte[header.size];
|
||||||
while (readCursor < payloadBytes.length - 1) {
|
in.get(payloadBytes, 0, header.size);
|
||||||
int bytesRead = in.read(payloadBytes, readCursor, header.size - readCursor);
|
|
||||||
if (bytesRead == -1) {
|
|
||||||
throw new IOException("Socket is disconnected");
|
|
||||||
}
|
|
||||||
readCursor += bytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the checksum.
|
// Verify the checksum.
|
||||||
byte[] hash;
|
byte[] hash;
|
||||||
@@ -246,17 +240,13 @@ public class BitcoinSerializer {
|
|||||||
return message;
|
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.
|
int magicCursor = 3; // Which byte of the magic we're looking for currently.
|
||||||
while (true) {
|
while (true) {
|
||||||
int b = in.read(); // Read a byte.
|
byte b = in.get();
|
||||||
if (b == -1) {
|
|
||||||
// There's no more data to read.
|
|
||||||
throw new IOException("Socket is disconnected");
|
|
||||||
}
|
|
||||||
// We're looking for a run of bytes that is the same as the packet magic but we want to ignore partial
|
// 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.
|
// 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) {
|
if (b == expectedByte) {
|
||||||
magicCursor--;
|
magicCursor--;
|
||||||
if (magicCursor < 0) {
|
if (magicCursor < 0) {
|
||||||
@@ -287,22 +277,17 @@ public class BitcoinSerializer {
|
|||||||
|
|
||||||
|
|
||||||
public static class BitcoinPacketHeader {
|
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 byte[] header;
|
||||||
public final String command;
|
public final String command;
|
||||||
public final int size;
|
public final int size;
|
||||||
public final byte[] checksum;
|
public final byte[] checksum;
|
||||||
|
|
||||||
public BitcoinPacketHeader(InputStream in) throws ProtocolException, IOException {
|
public BitcoinPacketHeader(ByteBuffer in) throws ProtocolException, BufferUnderflowException {
|
||||||
header = new byte[COMMAND_LEN + 4 + 4];
|
header = new byte[HEADER_LENGTH];
|
||||||
int readCursor = 0;
|
in.get(header, 0, header.length);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
int cursor = 0;
|
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;
|
package com.google.bitcoin.core;
|
||||||
|
|
||||||
import com.google.bitcoin.params.MainNetParams;
|
import com.google.bitcoin.params.*;
|
||||||
import com.google.bitcoin.params.TestNet2Params;
|
|
||||||
import com.google.bitcoin.params.TestNet3Params;
|
|
||||||
import com.google.bitcoin.params.UnitTestParams;
|
|
||||||
import com.google.bitcoin.script.Script;
|
import com.google.bitcoin.script.Script;
|
||||||
import com.google.bitcoin.script.ScriptOpCodes;
|
import com.google.bitcoin.script.ScriptOpCodes;
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
@@ -162,6 +159,12 @@ public abstract class NetworkParameters implements Serializable {
|
|||||||
return UnitTestParams.get();
|
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
|
* 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.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import net.jcip.annotations.GuardedBy;
|
import net.jcip.annotations.GuardedBy;
|
||||||
import org.jboss.netty.channel.*;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.ConnectException;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
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;
|
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 {
|
public class Peer extends PeerSocketHandler {
|
||||||
interface PeerLifecycleListener {
|
|
||||||
/** Called when the peer is connected */
|
|
||||||
public void onPeerConnected(Peer peer);
|
|
||||||
/** Called when the peer is disconnected */
|
|
||||||
public void onPeerDisconnected(Peer peer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Peer.class);
|
private static final Logger log = LoggerFactory.getLogger(Peer.class);
|
||||||
|
|
||||||
protected final ReentrantLock lock = Threading.lock("peer");
|
protected final ReentrantLock lock = Threading.lock("peer");
|
||||||
|
|
||||||
private final NetworkParameters params;
|
private final NetworkParameters params;
|
||||||
private final AbstractBlockChain blockChain;
|
private final AbstractBlockChain blockChain;
|
||||||
private volatile PeerAddress vAddress;
|
|
||||||
private final CopyOnWriteArrayList<ListenerRegistration<PeerEventListener>> eventListeners;
|
// onPeerDisconnected should not be called directly by Peers when a PeerGroup is involved (we don't know the total
|
||||||
private final CopyOnWriteArrayList<PeerLifecycleListener> lifecycleListeners;
|
// 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
|
// 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
|
// primary peer. This is to avoid redundant work and concurrency problems with downloading the same chain
|
||||||
// in parallel.
|
// in parallel.
|
||||||
@@ -130,46 +137,74 @@ public class Peer {
|
|||||||
private final CopyOnWriteArrayList<PendingPing> pendingPings;
|
private final CopyOnWriteArrayList<PendingPing> pendingPings;
|
||||||
private static final int PING_MOVING_AVERAGE_WINDOW = 20;
|
private static final int PING_MOVING_AVERAGE_WINDOW = 20;
|
||||||
|
|
||||||
private volatile Channel vChannel;
|
|
||||||
private volatile VersionMessage vPeerVersionMessage;
|
private volatile VersionMessage vPeerVersionMessage;
|
||||||
private boolean isAcked;
|
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) {
|
public Peer(NetworkParameters params, VersionMessage ver, @Nullable AbstractBlockChain chain, InetSocketAddress remoteAddress) {
|
||||||
this(params, chain, ver, null);
|
this(params, ver, remoteAddress, chain, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a peer that reads/writes from the given block chain and memory pool. Transactions stored
|
* <p>Construct a peer that reads/writes from the given block chain and memory pool. Transactions stored in a memory
|
||||||
* in a memory pool will have their confidence levels updated when a peer announces it, to reflect the greater
|
* pool will have their confidence levels updated when a peer announces it, to reflect the greater likelyhood that
|
||||||
* likelyhood that the transaction is valid.
|
* 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.params = Preconditions.checkNotNull(params);
|
||||||
this.versionMessage = Preconditions.checkNotNull(ver);
|
this.versionMessage = Preconditions.checkNotNull(ver);
|
||||||
this.blockChain = chain; // Allowed to be null.
|
this.blockChain = chain; // Allowed to be null.
|
||||||
this.vDownloadData = chain != null;
|
this.vDownloadData = chain != null;
|
||||||
this.getDataFutures = new CopyOnWriteArrayList<GetDataRequest>();
|
this.getDataFutures = new CopyOnWriteArrayList<GetDataRequest>();
|
||||||
this.eventListeners = new CopyOnWriteArrayList<ListenerRegistration<PeerEventListener>>();
|
this.eventListeners = new CopyOnWriteArrayList<PeerListenerRegistration>();
|
||||||
this.lifecycleListeners = new CopyOnWriteArrayList<PeerLifecycleListener>();
|
|
||||||
this.fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds();
|
this.fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds();
|
||||||
this.isAcked = false;
|
this.isAcked = false;
|
||||||
this.handler = new PeerHandler();
|
|
||||||
this.pendingPings = new CopyOnWriteArrayList<PendingPing>();
|
this.pendingPings = new CopyOnWriteArrayList<PendingPing>();
|
||||||
this.wallets = new CopyOnWriteArrayList<Wallet>();
|
this.wallets = new CopyOnWriteArrayList<Wallet>();
|
||||||
this.memoryPool = mempool;
|
this.memoryPool = mempool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a peer that reads/writes from the given chain. Automatically creates a VersionMessage for you from the
|
* <p>Construct a peer that reads/writes from the given chain. Automatically creates a VersionMessage for you from
|
||||||
* given software name/version strings, which should be something like "MySimpleTool", "1.0" and which will tell the
|
* the given software name/version strings, which should be something like "MySimpleTool", "1.0" and which will tell
|
||||||
* remote node to relay transaction inv messages before it has received a filter.
|
* 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) {
|
public Peer(NetworkParameters params, AbstractBlockChain blockChain, InetSocketAddress remoteAddress, String thisSoftwareName, String thisSoftwareVersion) {
|
||||||
this(params, blockChain, new VersionMessage(params, blockChain.getBestChainHeight(), true));
|
this(params, new VersionMessage(params, blockChain.getBestChainHeight(), true), blockChain, remoteAddress);
|
||||||
this.versionMessage.appendToSubVer(thisSoftwareName, thisSoftwareVersion, null);
|
this.versionMessage.appendToSubVer(thisSoftwareName, thisSoftwareVersion, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,24 +226,21 @@ public class Peer {
|
|||||||
* threads in order to get the results of those hook methods.
|
* threads in order to get the results of those hook methods.
|
||||||
*/
|
*/
|
||||||
public void addEventListener(PeerEventListener listener, Executor executor) {
|
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) {
|
public boolean removeEventListener(PeerEventListener listener) {
|
||||||
return ListenerRegistration.removeFromList(listener, eventListeners);
|
return ListenerRegistration.removeFromList(listener, eventListeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addLifecycleListener(PeerLifecycleListener listener) {
|
|
||||||
lifecycleListeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean removeLifecycleListener(PeerLifecycleListener listener) {
|
|
||||||
return lifecycleListeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
PeerAddress addr = vAddress;
|
PeerAddress addr = getAddress();
|
||||||
if (addr == null) {
|
if (addr == null) {
|
||||||
// User-provided NetworkConnection object.
|
// User-provided NetworkConnection object.
|
||||||
return "Peer()";
|
return "Peer()";
|
||||||
@@ -217,59 +249,40 @@ public class Peer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyDisconnect() {
|
@Override
|
||||||
for (PeerLifecycleListener listener : lifecycleListeners) {
|
public void connectionClosed() {
|
||||||
listener.onPeerDisconnected(Peer.this);
|
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
|
||||||
@Override
|
public void connectionOpened() {
|
||||||
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
|
// Announce ourselves. This has to come first to connect to clients beyond v0.3.20.2 which wait to hear
|
||||||
super.channelClosed(ctx, e);
|
// from us until they send their version message back.
|
||||||
notifyDisconnect();
|
PeerAddress address = getAddress();
|
||||||
}
|
log.info("Announcing to {} as: {}", address == null ? "Peer" : address.toSocketAddress(), versionMessage.subVer);
|
||||||
|
sendMessage(versionMessage);
|
||||||
@Override
|
connectionOpenFuture.set(this);
|
||||||
public void connectRequested(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
|
// When connecting, the remote peer sends us a version message with various bits of
|
||||||
vAddress = new PeerAddress((InetSocketAddress)e.getValue());
|
// useful data in it. We need to know the peer protocol version before we can talk to it.
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// Allow event listeners to filter the message stream. Listeners are allowed to drop messages by
|
||||||
// returning null.
|
// returning null.
|
||||||
for (ListenerRegistration<PeerEventListener> registration : eventListeners) {
|
for (ListenerRegistration<PeerEventListener> registration : eventListeners) {
|
||||||
@@ -312,7 +325,7 @@ public class Peer {
|
|||||||
} else if (m instanceof AlertMessage) {
|
} else if (m instanceof AlertMessage) {
|
||||||
processAlert((AlertMessage) m);
|
processAlert((AlertMessage) m);
|
||||||
} else if (m instanceof VersionMessage) {
|
} else if (m instanceof VersionMessage) {
|
||||||
vPeerVersionMessage = (VersionMessage) m;
|
processVersionMessage((VersionMessage) m);
|
||||||
} else if (m instanceof VersionAck) {
|
} else if (m instanceof VersionAck) {
|
||||||
if (vPeerVersionMessage == null) {
|
if (vPeerVersionMessage == null) {
|
||||||
throw new ProtocolException("got a version ack before version");
|
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");
|
throw new ProtocolException("got more than one version ack");
|
||||||
}
|
}
|
||||||
isAcked = true;
|
isAcked = true;
|
||||||
for (PeerLifecycleListener listener : lifecycleListeners)
|
this.setTimeoutEnabled(false);
|
||||||
listener.onPeerConnected(this);
|
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
|
// We check min version after onPeerConnected as channel.close() will
|
||||||
// call onPeerDisconnected, and we should probably call onPeerConnected first.
|
// call onPeerDisconnected, and we should probably call onPeerConnected first.
|
||||||
final int version = vMinProtocolVersion;
|
final int version = vMinProtocolVersion;
|
||||||
if (vPeerVersionMessage.clientVersion < version) {
|
if (vPeerVersionMessage.clientVersion < version) {
|
||||||
log.warn("Connected to a peer speaking protocol version {} but need {}, closing",
|
log.warn("Connected to a peer speaking protocol version {} but need {}, closing",
|
||||||
vPeerVersionMessage.clientVersion, version);
|
vPeerVersionMessage.clientVersion, version);
|
||||||
e.getChannel().close();
|
close();
|
||||||
}
|
}
|
||||||
} else if (m instanceof Ping) {
|
} else if (m instanceof Ping) {
|
||||||
if (((Ping) m).hasNonce())
|
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
|
// 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
|
// 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
|
// 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. */
|
private void processHeaders(HeadersMessage m) throws ProtocolException {
|
||||||
public PeerHandler getHandler() {
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processHeaders(HeadersMessage m) throws IOException, ProtocolException {
|
|
||||||
// Runs in network loop thread for this peer.
|
// 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
|
// 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 {
|
private void processGetData(GetDataMessage getdata) {
|
||||||
log.info("{}: Received getdata message: {}", vAddress, getdata.toString());
|
log.info("{}: Received getdata message: {}", getAddress(), getdata.toString());
|
||||||
ArrayList<Message> items = new ArrayList<Message>();
|
ArrayList<Message> items = new ArrayList<Message>();
|
||||||
for (ListenerRegistration<PeerEventListener> registration : eventListeners) {
|
for (ListenerRegistration<PeerEventListener> registration : eventListeners) {
|
||||||
if (registration.executor != Threading.SAME_THREAD) continue;
|
if (registration.executor != Threading.SAME_THREAD) continue;
|
||||||
@@ -487,19 +531,19 @@ public class Peer {
|
|||||||
if (items.size() == 0) {
|
if (items.size() == 0) {
|
||||||
return;
|
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) {
|
for (Message item : items) {
|
||||||
sendMessage(item);
|
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.
|
// Check a few basic syntax issues to ensure the received TX isn't nonsense.
|
||||||
tx.verify();
|
tx.verify();
|
||||||
final Transaction fTx;
|
final Transaction fTx;
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
log.debug("{}: Received tx {}", vAddress, tx.getHashAsString());
|
log.debug("{}: Received tx {}", getAddress(), tx.getHashAsString());
|
||||||
if (memoryPool != null) {
|
if (memoryPool != null) {
|
||||||
// We may get back a different transaction object.
|
// We may get back a different transaction object.
|
||||||
tx = memoryPool.seen(tx, getAddress());
|
tx = memoryPool.seen(tx, getAddress());
|
||||||
@@ -537,11 +581,11 @@ public class Peer {
|
|||||||
Futures.addCallback(downloadDependencies(fTx), new FutureCallback<List<Transaction>>() {
|
Futures.addCallback(downloadDependencies(fTx), new FutureCallback<List<Transaction>>() {
|
||||||
public void onSuccess(List<Transaction> dependencies) {
|
public void onSuccess(List<Transaction> dependencies) {
|
||||||
try {
|
try {
|
||||||
log.info("{}: Dependency download complete!", vAddress);
|
log.info("{}: Dependency download complete!", getAddress());
|
||||||
wallet.receivePending(fTx, dependencies);
|
wallet.receivePending(fTx, dependencies);
|
||||||
} catch (VerificationException e) {
|
} catch (VerificationException e) {
|
||||||
log.error("{}: Wallet failed to process pending transaction {}",
|
log.error("{}: Wallet failed to process pending transaction {}",
|
||||||
vAddress, fTx.getHashAsString());
|
getAddress(), fTx.getHashAsString());
|
||||||
log.error("Error was: ", e);
|
log.error("Error was: ", e);
|
||||||
// Not much more we can do at this point.
|
// 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.");
|
checkNotNull(memoryPool, "Must have a configured MemoryPool object to download dependencies.");
|
||||||
TransactionConfidence.ConfidenceType txConfidence = tx.getConfidence().getConfidenceType();
|
TransactionConfidence.ConfidenceType txConfidence = tx.getConfidence().getConfidenceType();
|
||||||
Preconditions.checkArgument(txConfidence != TransactionConfidence.ConfidenceType.BUILDING);
|
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>();
|
final LinkedList<Transaction> results = new LinkedList<Transaction>();
|
||||||
// future will be invoked when the entire dependency tree has been walked and the results compiled.
|
// future will be invoked when the entire dependency tree has been walked and the results compiled.
|
||||||
final ListenableFuture future = downloadDependenciesInternal(tx, new Object(), results);
|
final ListenableFuture future = downloadDependenciesInternal(tx, new Object(), results);
|
||||||
@@ -646,7 +690,7 @@ public class Peer {
|
|||||||
GetDataMessage getdata = new GetDataMessage(params);
|
GetDataMessage getdata = new GetDataMessage(params);
|
||||||
final long nonce = (long)(Math.random()*Long.MAX_VALUE);
|
final long nonce = (long)(Math.random()*Long.MAX_VALUE);
|
||||||
if (needToRequest.size() > 1)
|
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) {
|
for (Sha256Hash hash : needToRequest) {
|
||||||
getdata.addTransaction(hash);
|
getdata.addTransaction(hash);
|
||||||
GetDataRequest req = new GetDataRequest();
|
GetDataRequest req = new GetDataRequest();
|
||||||
@@ -670,7 +714,7 @@ public class Peer {
|
|||||||
List<ListenableFuture<Object>> childFutures = Lists.newLinkedList();
|
List<ListenableFuture<Object>> childFutures = Lists.newLinkedList();
|
||||||
for (Transaction tx : transactions) {
|
for (Transaction tx : transactions) {
|
||||||
if (tx == null) continue;
|
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);
|
results.add(tx);
|
||||||
// Now recurse into the dependencies of this transaction too.
|
// Now recurse into the dependencies of this transaction too.
|
||||||
childFutures.add(downloadDependenciesInternal(tx, marker, results));
|
childFutures.add(downloadDependenciesInternal(tx, marker, results));
|
||||||
@@ -727,9 +771,9 @@ public class Peer {
|
|||||||
return resultFuture;
|
return resultFuture;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processBlock(Block m) throws IOException {
|
private void processBlock(Block m) {
|
||||||
if (log.isDebugEnabled()) {
|
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()?
|
// Was this block requested by getBlock()?
|
||||||
if (maybeHandleRequestedData(m)) return;
|
if (maybeHandleRequestedData(m)) return;
|
||||||
@@ -739,7 +783,7 @@ public class Peer {
|
|||||||
}
|
}
|
||||||
// Did we lose download peer status after requesting block data?
|
// Did we lose download peer status after requesting block data?
|
||||||
if (!vDownloadData) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
pendingBlockDownloads.remove(m.getHash());
|
pendingBlockDownloads.remove(m.getHash());
|
||||||
@@ -781,7 +825,7 @@ public class Peer {
|
|||||||
}
|
}
|
||||||
} catch (VerificationException e) {
|
} catch (VerificationException e) {
|
||||||
// We don't want verification failures to kill the thread.
|
// 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) {
|
} catch (PrunedException e) {
|
||||||
// Unreachable when in SPV mode.
|
// Unreachable when in SPV mode.
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
@@ -789,12 +833,12 @@ public class Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix this duplication.
|
// TODO: Fix this duplication.
|
||||||
private void endFilteredBlock(FilteredBlock m) throws IOException {
|
private void endFilteredBlock(FilteredBlock m) {
|
||||||
if (log.isDebugEnabled()) {
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (blockChain == null) {
|
if (blockChain == null) {
|
||||||
@@ -850,7 +894,7 @@ public class Peer {
|
|||||||
}
|
}
|
||||||
} catch (VerificationException e) {
|
} catch (VerificationException e) {
|
||||||
// We don't want verification failures to kill the thread.
|
// 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) {
|
} catch (PrunedException e) {
|
||||||
// We pruned away some of the data we need to properly handle this block. We need to request the needed
|
// 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.
|
// 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();
|
List<InventoryItem> items = inv.getItems();
|
||||||
|
|
||||||
// Separate out the blocks and transactions, we'll handle them differently
|
// 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.
|
// Some other peer already announced this so don't download.
|
||||||
it.remove();
|
it.remove();
|
||||||
} else {
|
} else {
|
||||||
log.debug("{}: getdata on tx {}", vAddress, item.hash);
|
log.debug("{}: getdata on tx {}", getAddress(), item.hash);
|
||||||
getdata.addItem(item);
|
getdata.addItem(item);
|
||||||
}
|
}
|
||||||
// This can trigger transaction confidence listeners.
|
// 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
|
* 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.
|
* 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.
|
// This does not need to be locked.
|
||||||
log.info("Request to fetch block {}", blockHash);
|
log.info("Request to fetch block {}", blockHash);
|
||||||
GetDataMessage getdata = new GetDataMessage(params);
|
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,
|
* 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.
|
* 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.
|
// This does not need to be locked.
|
||||||
// TODO: Unit test this method.
|
// TODO: Unit test this method.
|
||||||
log.info("Request to fetch peer mempool tx {}", hash);
|
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. */
|
/** 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.
|
// This does not need to be locked.
|
||||||
Preconditions.checkArgument(getdata.getItems().size() == 1);
|
Preconditions.checkArgument(getdata.getItems().size() == 1);
|
||||||
GetDataRequest req = new GetDataRequest();
|
GetDataRequest req = new GetDataRequest();
|
||||||
@@ -1095,21 +1139,13 @@ public class Peer {
|
|||||||
wallets.remove(wallet);
|
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
|
// Keep track of the last request we made to the peer in blockChainDownloadLocked so we can avoid redundant and harmful
|
||||||
// getblocks requests.
|
// getblocks requests.
|
||||||
@GuardedBy("lock")
|
@GuardedBy("lock")
|
||||||
private Sha256Hash lastGetBlocksBegin, lastGetBlocksEnd;
|
private Sha256Hash lastGetBlocksBegin, lastGetBlocksEnd;
|
||||||
|
|
||||||
@GuardedBy("lock")
|
@GuardedBy("lock")
|
||||||
private void blockChainDownloadLocked(Sha256Hash toHash) throws IOException {
|
private void blockChainDownloadLocked(Sha256Hash toHash) {
|
||||||
checkState(lock.isHeldByCurrentThread());
|
checkState(lock.isHeldByCurrentThread());
|
||||||
// The block chain download process is a bit complicated. Basically, we start with one or more blocks in a
|
// 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
|
// 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
|
* 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.
|
* 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);
|
setDownloadData(true);
|
||||||
// TODO: peer might still have blocks that we don't have, and even have a heavier
|
// 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.
|
// chain even if the chain block count is lower.
|
||||||
@@ -1271,11 +1307,11 @@ public class Peer {
|
|||||||
* updated.
|
* updated.
|
||||||
* @throws ProtocolException if the peer version is too low to support measurable pings.
|
* @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));
|
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;
|
final VersionMessage ver = vPeerVersionMessage;
|
||||||
if (!ver.isPingPongSupported())
|
if (!ver.isPingPongSupported())
|
||||||
throw new ProtocolException("Peer version is too low for measurable pings: " + ver);
|
throw new ProtocolException("Peer version is too low for measurable pings: " + ver);
|
||||||
@@ -1366,13 +1402,6 @@ public class Peer {
|
|||||||
this.vDownloadData = downloadData;
|
this.vDownloadData = downloadData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the IP address and port of peer.
|
|
||||||
*/
|
|
||||||
public PeerAddress getAddress() {
|
|
||||||
return vAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns version data announced by the remote peer. */
|
/** Returns version data announced by the remote peer. */
|
||||||
public VersionMessage getPeerVersionMessage() {
|
public VersionMessage getPeerVersionMessage() {
|
||||||
return vPeerVersionMessage;
|
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
|
* The minimum P2P protocol version that is accepted. If the peer speaks a protocol version lower than this, it
|
||||||
* will be disconnected.
|
* 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;
|
this.vMinProtocolVersion = minProtocolVersion;
|
||||||
if (getVersionMessage().clientVersion < minProtocolVersion) {
|
if (getVersionMessage().clientVersion < minProtocolVersion) {
|
||||||
log.warn("{}: Disconnecting due to new min protocol version {}", this, minProtocolVersion);
|
log.warn("{}: Disconnecting due to new min protocol version {}", this, minProtocolVersion);
|
||||||
return Channels.close(vChannel);
|
close();
|
||||||
} else {
|
return true;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
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
|
* <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>
|
* 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");
|
checkNotNull(filter, "Clearing filters is not currently supported");
|
||||||
final VersionMessage ver = vPeerVersionMessage;
|
final VersionMessage ver = vPeerVersionMessage;
|
||||||
if (ver == null || !ver.isBloomFilteringSupported())
|
if (ver == null || !ver.isBloomFilteringSupported())
|
||||||
@@ -1428,13 +1457,8 @@ public class Peer {
|
|||||||
vBloomFilter = filter;
|
vBloomFilter = filter;
|
||||||
boolean shouldQueryMemPool = memoryPool != null || vDownloadData;
|
boolean shouldQueryMemPool = memoryPool != null || vDownloadData;
|
||||||
log.info("{}: Sending Bloom filter{}", this, shouldQueryMemPool ? " and querying mempool" : "");
|
log.info("{}: Sending Bloom filter{}", this, shouldQueryMemPool ? " and querying mempool" : "");
|
||||||
ChannelFuture future = sendMessage(filter);
|
sendMessage(filter);
|
||||||
if (shouldQueryMemPool)
|
sendMessage(new MemoryPoolMessage());
|
||||||
future.addListener(new ChannelFutureListener() {
|
|
||||||
public void operationComplete(ChannelFuture future) throws Exception {
|
|
||||||
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},
|
* 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 peer
|
||||||
* @param peerCount the total number of connected peers
|
* @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
|
* 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 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 peer
|
||||||
* @param peerCount the total number of connected peers
|
* @param peerCount the total number of connected peers
|
||||||
@@ -79,8 +79,11 @@ public interface PeerEventListener {
|
|||||||
public void onTransaction(Peer peer, Transaction t);
|
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
|
* <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.
|
* 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);
|
public List<Message> getData(Peer peer, GetDataMessage m);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,30 +17,24 @@
|
|||||||
|
|
||||||
package com.google.bitcoin.core;
|
package com.google.bitcoin.core;
|
||||||
|
|
||||||
import com.google.bitcoin.core.Peer.PeerHandler;
|
|
||||||
import com.google.bitcoin.discovery.PeerDiscovery;
|
import com.google.bitcoin.discovery.PeerDiscovery;
|
||||||
import com.google.bitcoin.discovery.PeerDiscoveryException;
|
import com.google.bitcoin.discovery.PeerDiscoveryException;
|
||||||
import com.google.bitcoin.script.Script;
|
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.ListenerRegistration;
|
||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.google.common.util.concurrent.*;
|
import com.google.common.util.concurrent.*;
|
||||||
import net.jcip.annotations.GuardedBy;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.IOException;
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.SocketAddress;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
@@ -83,7 +77,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
|
|||||||
private final CopyOnWriteArrayList<Peer> peers;
|
private final CopyOnWriteArrayList<Peer> peers;
|
||||||
// Currently connecting peers.
|
// Currently connecting peers.
|
||||||
private final CopyOnWriteArrayList<Peer> pendingPeers;
|
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.
|
// The peer that has been selected for the purposes of downloading announced data.
|
||||||
@GuardedBy("lock") private Peer downloadPeer;
|
@GuardedBy("lock") private Peer downloadPeer;
|
||||||
@@ -126,7 +120,6 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private ClientBootstrap bootstrap;
|
|
||||||
private int minBroadcastConnections = 0;
|
private int minBroadcastConnections = 0;
|
||||||
private AbstractWalletEventListener walletEventListener = new AbstractWalletEventListener() {
|
private AbstractWalletEventListener walletEventListener = new AbstractWalletEventListener() {
|
||||||
private void onChanged() {
|
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(); }
|
@Override public void onCoinsSent(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { onChanged(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
private class PeerStartupListener implements Peer.PeerLifecycleListener {
|
private class PeerStartupListener extends AbstractPeerEventListener {
|
||||||
public void onPeerConnected(Peer peer) {
|
@Override
|
||||||
|
public void onPeerConnected(Peer peer, int peerCount) {
|
||||||
handleNewPeer(peer);
|
handleNewPeer(peer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onPeerDisconnected(Peer peer) {
|
@Override
|
||||||
|
public void onPeerDisconnected(Peer peer, int peerCount) {
|
||||||
// The channel will be automatically removed from channels.
|
// The channel will be automatically removed from channels.
|
||||||
handlePeerDeath(peer);
|
handlePeerDeath(peer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visible for testing
|
// 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
|
// A bloom filter generated from all connected wallets that is given to new peers
|
||||||
private BloomFilter bloomFilter;
|
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 final long bloomFilterTweak = (long) (Math.random() * Long.MAX_VALUE);
|
||||||
private int lastBloomFilterElementCount;
|
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
|
* 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
|
* 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
|
* 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.
|
* and downloaded. This is probably the constructor you want to use.
|
||||||
*/
|
*/
|
||||||
public PeerGroup(NetworkParameters params, AbstractBlockChain chain) {
|
public PeerGroup(NetworkParameters params, @Nullable AbstractBlockChain chain) {
|
||||||
this(params, chain, null);
|
this(params, chain, new NioClientManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Creates a PeerGroup for the given network and chain, using the provided Netty {@link ClientBootstrap} object.
|
* Creates a new PeerGroup allowing you to specify the {@link ClientConnectionManager} which is used to create new
|
||||||
* </p>
|
* connections and keep track of existing ones.
|
||||||
*
|
|
||||||
* <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>
|
|
||||||
*/
|
*/
|
||||||
public PeerGroup(NetworkParameters params, @Nullable AbstractBlockChain chain, @Nullable ClientBootstrap bootstrap) {
|
public PeerGroup(NetworkParameters params, @Nullable AbstractBlockChain chain, ClientConnectionManager connectionManager) {
|
||||||
this.params = checkNotNull(params);
|
this.params = checkNotNull(params);
|
||||||
this.chain = chain;
|
this.chain = chain;
|
||||||
this.fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds();
|
this.fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds();
|
||||||
@@ -219,64 +204,14 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
|
|||||||
|
|
||||||
memoryPool = new MemoryPool();
|
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>();
|
inactives = new ArrayList<PeerAddress>();
|
||||||
peers = new CopyOnWriteArrayList<Peer>();
|
peers = new CopyOnWriteArrayList<Peer>();
|
||||||
pendingPeers = new CopyOnWriteArrayList<Peer>();
|
pendingPeers = new CopyOnWriteArrayList<Peer>();
|
||||||
channels = new DefaultChannelGroup();
|
channels = connectionManager;
|
||||||
peerDiscoverers = new CopyOnWriteArraySet<PeerDiscovery>();
|
peerDiscoverers = new CopyOnWriteArraySet<PeerDiscovery>();
|
||||||
peerEventListeners = new CopyOnWriteArrayList<ListenerRegistration<PeerEventListener>>();
|
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
|
* 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
|
* 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();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
// We may now have too many or too few open connections. Add more or drop some to get to the right amount.
|
// 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) {
|
while (adjustment > 0) {
|
||||||
try {
|
try {
|
||||||
connectToAnyPeer();
|
connectToAnyPeer();
|
||||||
@@ -301,10 +236,8 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
|
|||||||
}
|
}
|
||||||
adjustment--;
|
adjustment--;
|
||||||
}
|
}
|
||||||
while (adjustment < 0) {
|
if (adjustment < 0)
|
||||||
channels.iterator().next().close();
|
channels.closeConnections(-adjustment);
|
||||||
adjustment++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The maximum number of connections that we will create to peers. */
|
/** 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 {
|
protected void startUp() throws Exception {
|
||||||
// This is run in a background thread by the AbstractIdleService implementation.
|
// This is run in a background thread by the AbstractIdleService implementation.
|
||||||
vPingTimer = new Timer("Peer pinging thread", true);
|
vPingTimer = new Timer("Peer pinging thread", true);
|
||||||
|
channels.startAndWait();
|
||||||
// Bring up the requested number of connections. If a connect attempt fails,
|
// 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
|
// new peers will be tried until there is a success, so just calling connectToAnyPeer for the wanted number
|
||||||
// of peers is sufficient.
|
// of peers is sufficient.
|
||||||
@@ -593,11 +527,8 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
|
|||||||
protected void shutDown() throws Exception {
|
protected void shutDown() throws Exception {
|
||||||
// This is run on a separate thread by the AbstractIdleService implementation.
|
// This is run on a separate thread by the AbstractIdleService implementation.
|
||||||
vPingTimer.cancel();
|
vPingTimer.cancel();
|
||||||
// Blocking close of all sockets. TODO: there is a race condition here, for the solution see:
|
// Blocking close of all sockets.
|
||||||
// http://biasedbit.com/netty-releaseexternalresources-hangs/
|
channels.stopAndWait();
|
||||||
channels.close().await();
|
|
||||||
// All thread pools should be stopped by this call.
|
|
||||||
bootstrap.releaseExternalResources();
|
|
||||||
for (PeerDiscovery peerDiscovery : peerDiscoverers) {
|
for (PeerDiscovery peerDiscovery : peerDiscoverers) {
|
||||||
peerDiscovery.shutdown();
|
peerDiscovery.shutdown();
|
||||||
}
|
}
|
||||||
@@ -701,11 +632,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
|
|||||||
if (!filter.equals(bloomFilter)) {
|
if (!filter.equals(bloomFilter)) {
|
||||||
bloomFilter = filter;
|
bloomFilter = filter;
|
||||||
for (Peer peer : peers)
|
for (Peer peer : peers)
|
||||||
try {
|
peer.setBloomFilter(filter);
|
||||||
peer.setBloomFilter(filter);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Now adjust the earliest key time backwards by a week to handle the case of clock drift. This can occur
|
// 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.
|
* @param address destination IP and port.
|
||||||
* @return a ChannelFuture that can be used to wait for the socket to connect. A socket
|
* @return The newly created Peer object. Use {@link com.google.bitcoin.core.Peer#getConnectionOpenFuture()} if you
|
||||||
* connection does not mean that protocol handshake has occured.
|
* want a future which completes when the connection is open.
|
||||||
*/
|
*/
|
||||||
public ChannelFuture connectTo(SocketAddress address) {
|
public Peer connectTo(InetSocketAddress address) {
|
||||||
return connectTo(address, true);
|
return connectTo(address, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal version.
|
// Internal version.
|
||||||
protected ChannelFuture connectTo(SocketAddress address, boolean incrementMaxConnections) {
|
protected Peer connectTo(InetSocketAddress address, boolean incrementMaxConnections) {
|
||||||
ChannelFuture future = bootstrap.connect(address);
|
VersionMessage ver = getVersionMessage().duplicate();
|
||||||
// Make sure that the channel group gets access to the channel only if it connects successfully (otherwise
|
ver.bestHeight = chain == null ? 0 : chain.getBestChainHeight();
|
||||||
// it cannot be closed and trying to do so will cause problems).
|
ver.time = Utils.now().getTime() / 1000;
|
||||||
future.addListener(new ChannelFutureListener() {
|
|
||||||
public void operationComplete(ChannelFuture future) throws Exception {
|
Peer peer = new Peer(params, ver, address, chain, memoryPool);
|
||||||
if (future.isSuccess())
|
peer.addEventListener(startupListener, Threading.SAME_THREAD);
|
||||||
channels.add(future.getChannel());
|
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
|
// When the channel has connected and version negotiated successfully, handleNewPeer will end up being called on
|
||||||
// a worker thread.
|
// 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) {
|
if (incrementMaxConnections) {
|
||||||
// We don't use setMaxConnections here as that would trigger a recursive attempt to establish a new
|
// We don't use setMaxConnections here as that would trigger a recursive attempt to establish a new
|
||||||
// outbound connection.
|
// outbound connection.
|
||||||
@@ -789,15 +710,15 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
|
|||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return future;
|
return peer;
|
||||||
}
|
}
|
||||||
|
|
||||||
static public Peer peerFromChannelFuture(ChannelFuture future) {
|
/**
|
||||||
return peerFromChannel(future.getChannel());
|
* 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.
|
||||||
|
*/
|
||||||
static public Peer peerFromChannel(Channel channel) {
|
public void setConnectTimeoutMillis(int connectTimeoutMillis) {
|
||||||
return ((PeerHandler)channel.getPipeline().get("peer")).getPeer();
|
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
|
// 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
|
// 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.
|
// OK because it helps improve wallet privacy. Old nodes will just ignore the message.
|
||||||
try {
|
if (bloomFilter != null) peer.setBloomFilter(bloomFilter);
|
||||||
if (bloomFilter != null) peer.setBloomFilter(bloomFilter);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// That was quick...already disconnected
|
|
||||||
}
|
|
||||||
// Link the peer to the memory pool so broadcast transactions have their confidence levels updated.
|
// Link the peer to the memory pool so broadcast transactions have their confidence levels updated.
|
||||||
peer.setDownloadData(false);
|
peer.setDownloadData(false);
|
||||||
// TODO: The peer should calculate the fast catchup time from the added wallets here.
|
// 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);
|
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.
|
// And set up event listeners for clients. This will allow them to find out about new transactions and blocks.
|
||||||
for (ListenerRegistration<PeerEventListener> registration : peerEventListeners) {
|
for (ListenerRegistration<PeerEventListener> registration : peerEventListeners) {
|
||||||
peer.addEventListener(registration.listener, registration.executor);
|
peer.addEventListenerWithoutOnDisconnect(registration.listener, registration.executor);
|
||||||
}
|
}
|
||||||
setupPingingForNewPeer(peer);
|
setupPingingForNewPeer(peer);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1080,8 +997,6 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
|
|||||||
setDownloadPeer(peer);
|
setDownloadPeer(peer);
|
||||||
// startBlockChainDownload will setDownloadData(true) on itself automatically.
|
// startBlockChainDownload will setDownloadData(true) on itself automatically.
|
||||||
peer.startBlockChainDownload();
|
peer.startBlockChainDownload();
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("failed to start block chain download from " + peer, e);
|
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
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
|
// 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.
|
// better then we'll settle for the highest we found instead.
|
||||||
int highestVersion = 0, preferredVersion = 0;
|
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;
|
final int PREFERRED_VERSION = FilteredBlock.MIN_PROTOCOL_VERSION;
|
||||||
for (Peer peer : candidates) {
|
for (Peer peer : candidates) {
|
||||||
highestVersion = Math.max(peer.getPeerVersionMessage().clientVersion, highestVersion);
|
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 InsufficientMoneyException if the request could not be completed due to not enough balance.
|
||||||
* @throws IOException if there was a problem broadcasting the transaction
|
* @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);
|
Transaction tx = sendCoinsOffline(request);
|
||||||
peer.sendMessage(tx);
|
peer.sendMessage(tx);
|
||||||
return tx;
|
return tx;
|
||||||
|
|||||||
@@ -14,13 +14,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.google.bitcoin.protocols.niowrapper;
|
package com.google.bitcoin.networkabstraction;
|
||||||
|
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
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 {
|
public abstract class AbstractTimeoutHandler {
|
||||||
// TimerTask and timeout value which are added to a timer to kill the connection on timeout
|
// 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;
|
private boolean timeoutEnabled = true;
|
||||||
|
|
||||||
// A timer which manages expiring channels as their timeouts occur (if configured).
|
// 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
|
* <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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.google.bitcoin.protocols.niowrapper;
|
package com.google.bitcoin.networkabstraction;
|
||||||
|
|
||||||
import java.io.IOException;
|
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.ByteBuffer;
|
||||||
import java.nio.channels.AsynchronousCloseException;
|
import java.util.Set;
|
||||||
import java.nio.channels.ClosedChannelException;
|
|
||||||
import java.nio.channels.SocketChannel;
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkState;
|
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 {
|
public class BlockingClient implements MessageWriteTarget {
|
||||||
private static final org.slf4j.Logger log = LoggerFactory.getLogger(NioClient.class);
|
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_LOWER_BOUND = 4096;
|
||||||
private static final int BUFFER_SIZE_UPPER_BOUND = 65536;
|
private static final int BUFFER_SIZE_UPPER_BOUND = 65536;
|
||||||
|
|
||||||
@Nonnull private final ByteBuffer dbuf;
|
@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.
|
* <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
|
* @param connectTimeoutMillis The connect timeout set on the connection (in milliseconds). 0 is interpreted as no
|
||||||
* timeout.
|
* 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,
|
public BlockingClient(final SocketAddress serverAddress, final StreamParser parser,
|
||||||
final int connectTimeoutMillis) throws IOException {
|
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
|
// 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.
|
// 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));
|
dbuf = ByteBuffer.allocateDirect(Math.min(Math.max(parser.getMaxMessageSize(), BUFFER_SIZE_LOWER_BOUND), BUFFER_SIZE_UPPER_BOUND));
|
||||||
parser.setWriteTarget(this);
|
parser.setWriteTarget(this);
|
||||||
sc = SocketChannel.open();
|
socket = new Socket();
|
||||||
|
|
||||||
new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
if (clientSet != null)
|
||||||
|
clientSet.add(BlockingClient.this);
|
||||||
try {
|
try {
|
||||||
sc.socket().connect(serverAddress, connectTimeoutMillis);
|
socket.connect(serverAddress, connectTimeoutMillis);
|
||||||
parser.connectionOpened();
|
parser.connectionOpened();
|
||||||
|
InputStream stream = socket.getInputStream();
|
||||||
|
byte[] readBuff = new byte[dbuf.capacity()];
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
int read = sc.read(dbuf);
|
// TODO Kill the message duplication here
|
||||||
if (read == 0)
|
checkState(dbuf.remaining() > 0 && dbuf.remaining() <= readBuff.length);
|
||||||
continue;
|
int read = stream.read(readBuff, 0, Math.max(1, Math.min(dbuf.remaining(), stream.available())));
|
||||||
else if (read == -1)
|
if (read == -1)
|
||||||
return;
|
return;
|
||||||
|
dbuf.put(readBuff, 0, read);
|
||||||
// "flip" the buffer - setting the limit to the current position and setting position to 0
|
// "flip" the buffer - setting the limit to the current position and setting position to 0
|
||||||
dbuf.flip();
|
dbuf.flip();
|
||||||
// Use parser.receiveBytes's return value as a double-check that it stopped reading at the right
|
// 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)
|
// position)
|
||||||
dbuf.compact();
|
dbuf.compact();
|
||||||
}
|
}
|
||||||
} catch (AsynchronousCloseException e) {// Expected if the connection is closed
|
|
||||||
} catch (ClosedChannelException e) { // Expected if the connection is closed
|
|
||||||
} catch (Exception e) {
|
} 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 {
|
} finally {
|
||||||
try {
|
try {
|
||||||
sc.close();
|
socket.close();
|
||||||
} catch (IOException e1) {
|
} catch (IOException e1) {
|
||||||
// At this point there isn't much we can do, and we can probably assume the channel is closed
|
// 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();
|
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() {
|
public void closeConnection() {
|
||||||
// Closes the channel, triggering an exception in the network-handling thread triggering connectionClosed()
|
// Closes the channel, triggering an exception in the network-handling thread triggering connectionClosed()
|
||||||
try {
|
try {
|
||||||
sc.close();
|
vCloseRequested = true;
|
||||||
|
socket.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes raw bytes to the channel (used by the write method in StreamParser)
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void writeBytes(byte[] message) {
|
public synchronized void writeBytes(byte[] message) throws IOException {
|
||||||
try {
|
try {
|
||||||
if (sc.write(ByteBuffer.wrap(message)) != message.length)
|
socket.getOutputStream().write(message);
|
||||||
throw new IOException("Couldn't write all of message to socket");
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Error writing message to connection, closing connection", e);
|
log.error("Error writing message to connection, closing connection", e);
|
||||||
closeConnection();
|
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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.google.bitcoin.protocols.niowrapper;
|
package com.google.bitcoin.networkabstraction;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@@ -22,6 +22,13 @@ import java.io.IOException;
|
|||||||
* A target to which messages can be written/connection can be closed
|
* A target to which messages can be written/connection can be closed
|
||||||
*/
|
*/
|
||||||
public interface MessageWriteTarget {
|
public interface MessageWriteTarget {
|
||||||
|
/**
|
||||||
|
* Writes the given bytes to the remote server.
|
||||||
|
*/
|
||||||
void writeBytes(byte[] message) throws IOException;
|
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();
|
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.
|
* 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.core.Utils;
|
||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import com.google.protobuf.MessageLite;
|
import com.google.protobuf.MessageLite;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -74,7 +75,7 @@ public class ProtobufParser<MessageType extends MessageLite> extends AbstractTim
|
|||||||
@GuardedBy("lock") private byte[] messageBytes;
|
@GuardedBy("lock") private byte[] messageBytes;
|
||||||
private final ReentrantLock lock = Threading.lock("ProtobufParser");
|
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.
|
* Creates a new protobuf handler.
|
||||||
@@ -14,12 +14,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.google.bitcoin.protocols.niowrapper;
|
package com.google.bitcoin.networkabstraction;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
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 {
|
public interface StreamParser {
|
||||||
/** Called when the connection socket is closed */
|
/** Called when the connection socket is closed */
|
||||||
@@ -29,14 +30,22 @@ public interface StreamParser {
|
|||||||
void connectionOpened();
|
void connectionOpened();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when new bytes are available from the remote end.
|
* <p>Called when new bytes are available from the remote end. This should only ever be called by the single
|
||||||
* * buff will start with its limit set to the position we can read to and its position set to the location we will
|
* writeTarget associated with any given StreamParser, multiple callers will likely confuse implementations.</p>
|
||||||
* start reading at
|
*
|
||||||
* * May read more than one message (recursively) if there are enough bytes available
|
* Implementers/callers must follow the following conventions exactly:
|
||||||
* * Uses messageBytes/messageBytesOffset to store message which are larger (incl their length prefix) than buff's
|
* <ul>
|
||||||
* capacity(), ie it is up to this method to ensure we dont run out of buffer space to decode the next message.
|
* <li>buff will start with its limit set to the position we can read to and its position set to the location we
|
||||||
* * buff will end with its limit the same as it was previously, and its position set to the position up to which
|
* will start reading at (always 0)</li>
|
||||||
* bytes have been read (the same as its return value)
|
* <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
|
* @return The amount of bytes consumed which should not be provided again
|
||||||
*/
|
*/
|
||||||
int receiveBytes(ByteBuffer buff) throws Exception;
|
int receiveBytes(ByteBuffer buff) throws Exception;
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.google.bitcoin.protocols.niowrapper;
|
package com.google.bitcoin.networkabstraction;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import javax.annotation.Nullable;
|
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.InsufficientMoneyException;
|
||||||
import com.google.bitcoin.core.Sha256Hash;
|
import com.google.bitcoin.core.Sha256Hash;
|
||||||
import com.google.bitcoin.core.Wallet;
|
import com.google.bitcoin.core.Wallet;
|
||||||
import com.google.bitcoin.protocols.niowrapper.NioClient;
|
import com.google.bitcoin.networkabstraction.NioClient;
|
||||||
import com.google.bitcoin.protocols.niowrapper.ProtobufParser;
|
import com.google.bitcoin.networkabstraction.ProtobufParser;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import org.bitcoin.paymentchannel.Protos;
|
import org.bitcoin.paymentchannel.Protos;
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ import javax.annotation.Nullable;
|
|||||||
import com.google.bitcoin.core.Sha256Hash;
|
import com.google.bitcoin.core.Sha256Hash;
|
||||||
import com.google.bitcoin.core.TransactionBroadcaster;
|
import com.google.bitcoin.core.TransactionBroadcaster;
|
||||||
import com.google.bitcoin.core.Wallet;
|
import com.google.bitcoin.core.Wallet;
|
||||||
import com.google.bitcoin.protocols.niowrapper.NioServer;
|
import com.google.bitcoin.networkabstraction.NioServer;
|
||||||
import com.google.bitcoin.protocols.niowrapper.ProtobufParser;
|
import com.google.bitcoin.networkabstraction.ProtobufParser;
|
||||||
import com.google.bitcoin.protocols.niowrapper.StreamParserFactory;
|
import com.google.bitcoin.networkabstraction.StreamParserFactory;
|
||||||
import org.bitcoin.paymentchannel.Protos;
|
import org.bitcoin.paymentchannel.Protos;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
@@ -48,7 +48,8 @@ public class PaymentChannelServerListener {
|
|||||||
private final HandlerFactory eventHandlerFactory;
|
private final HandlerFactory eventHandlerFactory;
|
||||||
private final BigInteger minAcceptedChannelSize;
|
private final BigInteger minAcceptedChannelSize;
|
||||||
|
|
||||||
private final NioServer server;
|
private NioServer server;
|
||||||
|
private final int timeoutSeconds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A factory which generates connection-specific event handlers.
|
* 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)
|
* @throws Exception If binding to the given port fails (eg SocketException: Permission denied for privileged ports)
|
||||||
*/
|
*/
|
||||||
public void bindAndStart(int port) throws Exception {
|
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.broadcaster = checkNotNull(broadcaster);
|
||||||
this.eventHandlerFactory = checkNotNull(eventHandlerFactory);
|
this.eventHandlerFactory = checkNotNull(eventHandlerFactory);
|
||||||
this.minAcceptedChannelSize = checkNotNull(minAcceptedChannelSize);
|
this.minAcceptedChannelSize = checkNotNull(minAcceptedChannelSize);
|
||||||
|
this.timeoutSeconds = timeoutSeconds;
|
||||||
server = new NioServer(new StreamParserFactory() {
|
|
||||||
@Override
|
|
||||||
public ProtobufParser getNewParser(InetAddress inetAddress, int port) {
|
|
||||||
return new ServerHandler(new InetSocketAddress(inetAddress, port), timeoutSeconds).socketProtobufHandler;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,10 +177,6 @@ public class PaymentChannelServerListener {
|
|||||||
* wallet.</p>
|
* wallet.</p>
|
||||||
*/
|
*/
|
||||||
public void close() {
|
public void close() {
|
||||||
try {
|
server.stopAndWait();
|
||||||
server.stop();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ package com.google.bitcoin.protocols.channels;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
import com.google.bitcoin.core.Sha256Hash;
|
import com.google.bitcoin.core.Sha256Hash;
|
||||||
import com.google.bitcoin.protocols.niowrapper.ProtobufParser;
|
import com.google.bitcoin.networkabstraction.ProtobufParser;
|
||||||
import org.bitcoin.paymentchannel.Protos;
|
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;
|
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;
|
ListenerRegistration<T> item = null;
|
||||||
for (ListenerRegistration<T> registration : list) {
|
for (ListenerRegistration<T> registration : list) {
|
||||||
if (registration.listener == listener) {
|
if (registration.listener == listener) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
public class TestUtils {
|
public class TestUtils {
|
||||||
public static Transaction createFakeTxWithChangeAddress(NetworkParameters params, BigInteger nanocoins, Address to, Address changeOutput)
|
public static Transaction createFakeTxWithChangeAddress(NetworkParameters params, BigInteger nanocoins, Address to, Address changeOutput)
|
||||||
@@ -107,7 +108,7 @@ public class TestUtils {
|
|||||||
BitcoinSerializer bs = new BitcoinSerializer(params);
|
BitcoinSerializer bs = new BitcoinSerializer(params);
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
bs.serialize(tx, bos);
|
bs.serialize(tx, bos);
|
||||||
return (Transaction) bs.deserialize(new ByteArrayInputStream(bos.toByteArray()));
|
return (Transaction) bs.deserialize(ByteBuffer.wrap(bos.toByteArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DoubleSpends {
|
public static class DoubleSpends {
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ import com.google.bitcoin.params.MainNetParams;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.spongycastle.util.encoders.Hex;
|
import org.spongycastle.util.encoders.Hex;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.nio.BufferUnderflowException;
|
||||||
import java.io.InputStream;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
@@ -57,15 +56,14 @@ public class BitcoinSerializerTest {
|
|||||||
public void testAddr() throws Exception {
|
public void testAddr() throws Exception {
|
||||||
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
|
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
|
||||||
// the actual data from https://en.bitcoin.it/wiki/Protocol_specification#addr
|
// the actual data from https://en.bitcoin.it/wiki/Protocol_specification#addr
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(addrMessage);
|
AddressMessage a = (AddressMessage)bs.deserialize(ByteBuffer.wrap(addrMessage));
|
||||||
AddressMessage a = (AddressMessage)bs.deserialize(bais);
|
|
||||||
assertEquals(1, a.getAddresses().size());
|
assertEquals(1, a.getAddresses().size());
|
||||||
PeerAddress pa = a.getAddresses().get(0);
|
PeerAddress pa = a.getAddresses().get(0);
|
||||||
assertEquals(8333, pa.getPort());
|
assertEquals(8333, pa.getPort());
|
||||||
assertEquals("10.0.0.1", pa.getAddr().getHostAddress());
|
assertEquals("10.0.0.1", pa.getAddr().getHostAddress());
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(addrMessage.length);
|
ByteArrayOutputStream bos = new ByteArrayOutputStream(addrMessage.length);
|
||||||
bs.serialize(a, bos);
|
bs.serialize(a, bos);
|
||||||
|
|
||||||
//this wont be true due to dynamic timestamps.
|
//this wont be true due to dynamic timestamps.
|
||||||
//assertTrue(LazyParseByteCacheTest.arrayContains(bos.toByteArray(), addrMessage));
|
//assertTrue(LazyParseByteCacheTest.arrayContains(bos.toByteArray(), addrMessage));
|
||||||
}
|
}
|
||||||
@@ -73,86 +71,81 @@ public class BitcoinSerializerTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testLazyParsing() throws Exception {
|
public void testLazyParsing() throws Exception {
|
||||||
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get(), true, false);
|
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get(), true, false);
|
||||||
|
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(txMessage);
|
Transaction tx = (Transaction)bs.deserialize(ByteBuffer.wrap(txMessage));
|
||||||
Transaction tx = (Transaction)bs.deserialize(bais);
|
|
||||||
assertNotNull(tx);
|
assertNotNull(tx);
|
||||||
assertEquals(false, tx.isParsed());
|
assertEquals(false, tx.isParsed());
|
||||||
assertEquals(true, tx.isCached());
|
assertEquals(true, tx.isCached());
|
||||||
tx.getInputs();
|
tx.getInputs();
|
||||||
assertEquals(true, tx.isParsed());
|
assertEquals(true, tx.isParsed());
|
||||||
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
bs.serialize(tx, bos);
|
bs.serialize(tx, bos);
|
||||||
assertEquals(true, Arrays.equals(txMessage, bos.toByteArray()));
|
assertEquals(true, Arrays.equals(txMessage, bos.toByteArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCachedParsing() throws Exception {
|
public void testCachedParsing() throws Exception {
|
||||||
testCachedParsing(true);
|
testCachedParsing(true);
|
||||||
testCachedParsing(false);
|
testCachedParsing(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testCachedParsing(boolean lazy) throws Exception {
|
private void testCachedParsing(boolean lazy) throws Exception {
|
||||||
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get(), lazy, true);
|
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get(), lazy, true);
|
||||||
|
|
||||||
//first try writing to a fields to ensure uncaching and children are not affected
|
//first try writing to a fields to ensure uncaching and children are not affected
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(txMessage);
|
Transaction tx = (Transaction)bs.deserialize(ByteBuffer.wrap(txMessage));
|
||||||
Transaction tx = (Transaction)bs.deserialize(bais);
|
|
||||||
assertNotNull(tx);
|
assertNotNull(tx);
|
||||||
assertEquals(!lazy, tx.isParsed());
|
assertEquals(!lazy, tx.isParsed());
|
||||||
assertEquals(true, tx.isCached());
|
assertEquals(true, tx.isCached());
|
||||||
|
|
||||||
tx.setLockTime(1);
|
tx.setLockTime(1);
|
||||||
//parent should have been uncached
|
//parent should have been uncached
|
||||||
assertEquals(false, tx.isCached());
|
assertEquals(false, tx.isCached());
|
||||||
//child should remain cached.
|
//child should remain cached.
|
||||||
assertEquals(true, tx.getInputs().get(0).isCached());
|
assertEquals(true, tx.getInputs().get(0).isCached());
|
||||||
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
bs.serialize(tx, bos);
|
bs.serialize(tx, bos);
|
||||||
assertEquals(true, !Arrays.equals(txMessage, bos.toByteArray()));
|
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
|
//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(ByteBuffer.wrap(txMessage));
|
||||||
tx = (Transaction)bs.deserialize(bais);
|
|
||||||
assertNotNull(tx);
|
assertNotNull(tx);
|
||||||
assertEquals(!lazy, tx.isParsed());
|
assertEquals(!lazy, tx.isParsed());
|
||||||
assertEquals(true, tx.isCached());
|
assertEquals(true, tx.isCached());
|
||||||
|
|
||||||
tx.getInputs().get(0).setSequenceNumber(1);
|
tx.getInputs().get(0).setSequenceNumber(1);
|
||||||
//parent should have been uncached
|
//parent should have been uncached
|
||||||
assertEquals(false, tx.isCached());
|
assertEquals(false, tx.isCached());
|
||||||
//so should child
|
//so should child
|
||||||
assertEquals(false, tx.getInputs().get(0).isCached());
|
assertEquals(false, tx.getInputs().get(0).isCached());
|
||||||
|
|
||||||
bos = new ByteArrayOutputStream();
|
bos = new ByteArrayOutputStream();
|
||||||
bs.serialize(tx, bos);
|
bs.serialize(tx, bos);
|
||||||
assertEquals(true, !Arrays.equals(txMessage, bos.toByteArray()));
|
assertEquals(true, !Arrays.equals(txMessage, bos.toByteArray()));
|
||||||
|
|
||||||
//deserialize/reserialize to check for equals.
|
//deserialize/reserialize to check for equals.
|
||||||
bais = new ByteArrayInputStream(txMessage);
|
tx = (Transaction)bs.deserialize(ByteBuffer.wrap(txMessage));
|
||||||
tx = (Transaction)bs.deserialize(bais);
|
|
||||||
assertNotNull(tx);
|
assertNotNull(tx);
|
||||||
assertEquals(!lazy, tx.isParsed());
|
assertEquals(!lazy, tx.isParsed());
|
||||||
assertEquals(true, tx.isCached());
|
assertEquals(true, tx.isCached());
|
||||||
bos = new ByteArrayOutputStream();
|
bos = new ByteArrayOutputStream();
|
||||||
bs.serialize(tx, bos);
|
bs.serialize(tx, bos);
|
||||||
assertEquals(true, Arrays.equals(txMessage, bos.toByteArray()));
|
assertEquals(true, Arrays.equals(txMessage, bos.toByteArray()));
|
||||||
|
|
||||||
//deserialize/reserialize to check for equals. Set a field to it's existing value to trigger uncache
|
//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(ByteBuffer.wrap(txMessage));
|
||||||
tx = (Transaction)bs.deserialize(bais);
|
|
||||||
assertNotNull(tx);
|
assertNotNull(tx);
|
||||||
assertEquals(!lazy, tx.isParsed());
|
assertEquals(!lazy, tx.isParsed());
|
||||||
assertEquals(true, tx.isCached());
|
assertEquals(true, tx.isCached());
|
||||||
|
|
||||||
tx.getInputs().get(0).setSequenceNumber(tx.getInputs().get(0).getSequenceNumber());
|
tx.getInputs().get(0).setSequenceNumber(tx.getInputs().get(0).getSequenceNumber());
|
||||||
|
|
||||||
bos = new ByteArrayOutputStream();
|
bos = new ByteArrayOutputStream();
|
||||||
bs.serialize(tx, bos);
|
bs.serialize(tx, bos);
|
||||||
assertEquals(true, Arrays.equals(txMessage, bos.toByteArray()));
|
assertEquals(true, Arrays.equals(txMessage, bos.toByteArray()));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -163,12 +156,10 @@ public class BitcoinSerializerTest {
|
|||||||
public void testHeaders1() throws Exception {
|
public void testHeaders1() throws Exception {
|
||||||
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
|
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
|
||||||
|
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(Hex.decode("f9beb4d9686561" +
|
HeadersMessage hm = (HeadersMessage) bs.deserialize(ByteBuffer.wrap(Hex.decode("f9beb4d9686561" +
|
||||||
"646572730000000000520000005d4fab8101010000006fe28c0ab6f1b372c1a6a246ae6" +
|
"646572730000000000520000005d4fab8101010000006fe28c0ab6f1b372c1a6a246ae6" +
|
||||||
"3f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677b" +
|
"3f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677b" +
|
||||||
"a1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e3629900"));
|
"a1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e3629900")));
|
||||||
|
|
||||||
HeadersMessage hm = (HeadersMessage) bs.deserialize(bais);
|
|
||||||
|
|
||||||
// The first block after the genesis
|
// The first block after the genesis
|
||||||
// http://blockexplorer.com/b/1
|
// http://blockexplorer.com/b/1
|
||||||
@@ -190,7 +181,7 @@ public class BitcoinSerializerTest {
|
|||||||
public void testHeaders2() throws Exception {
|
public void testHeaders2() throws Exception {
|
||||||
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
|
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
|
||||||
|
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(Hex.decode("f9beb4d96865616465" +
|
HeadersMessage hm = (HeadersMessage) bs.deserialize(ByteBuffer.wrap(Hex.decode("f9beb4d96865616465" +
|
||||||
"72730000000000e701000085acd4ea06010000006fe28c0ab6f1b372c1a6a246ae63f74f931e" +
|
"72730000000000e701000085acd4ea06010000006fe28c0ab6f1b372c1a6a246ae63f74f931e" +
|
||||||
"8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1c" +
|
"8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1c" +
|
||||||
"db606e857233e0e61bc6649ffff001d01e3629900010000004860eb18bf1b1620e37e9490fc8a" +
|
"db606e857233e0e61bc6649ffff001d01e3629900010000004860eb18bf1b1620e37e9490fc8a" +
|
||||||
@@ -203,9 +194,7 @@ public class BitcoinSerializerTest {
|
|||||||
"a88d221c8bd6c059da090e88f8a2c99690ee55dbba4e00000000e11c48fecdd9e72510ca84f023" +
|
"a88d221c8bd6c059da090e88f8a2c99690ee55dbba4e00000000e11c48fecdd9e72510ca84f023" +
|
||||||
"370c9a38bf91ac5cae88019bee94d24528526344c36649ffff001d1d03e4770001000000fc33f5" +
|
"370c9a38bf91ac5cae88019bee94d24528526344c36649ffff001d1d03e4770001000000fc33f5" +
|
||||||
"96f822a0a1951ffdbf2a897b095636ad871707bf5d3162729b00000000379dfb96a5ea8c81700ea4" +
|
"96f822a0a1951ffdbf2a897b095636ad871707bf5d3162729b00000000379dfb96a5ea8c81700ea4" +
|
||||||
"ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c9700"));
|
"ac6b97ae9a9312b2d4301a29580e924ee6761a2520adc46649ffff001d189c4c9700")));
|
||||||
|
|
||||||
HeadersMessage hm = (HeadersMessage) bs.deserialize(bais);
|
|
||||||
|
|
||||||
int nBlocks = hm.getBlockHeaders().size();
|
int nBlocks = hm.getBlockHeaders().size();
|
||||||
assertEquals(nBlocks, 6);
|
assertEquals(nBlocks, 6);
|
||||||
@@ -230,87 +219,32 @@ public class BitcoinSerializerTest {
|
|||||||
assertEquals(thirdBlock.getNonce(), 2850094635L);
|
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
|
@Test
|
||||||
public void testBitcoinPacketHeader() {
|
public void testBitcoinPacketHeader() {
|
||||||
BitcoinSerializer bs = new BitcoinSerializer(MainNetParams.get());
|
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(new byte[]{});
|
|
||||||
BitcoinSerializer.BitcoinPacketHeader bitcoinPacketHeader;
|
|
||||||
try {
|
try {
|
||||||
bitcoinPacketHeader = new BitcoinSerializer.BitcoinPacketHeader(bais);
|
new BitcoinSerializer.BitcoinPacketHeader(ByteBuffer.wrap(new byte[]{0}));
|
||||||
} catch (ProtocolException e) {
|
|
||||||
fail();
|
fail();
|
||||||
} catch (IOException e) {
|
} catch (BufferUnderflowException e) {
|
||||||
// expected
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message with a Message size which is 1 too big, in little endian format.
|
// Message with a Message size which is 1 too big, in little endian format.
|
||||||
byte[] wrongMessageLength = Hex.decode("000000000000000000000000010000020000000000");
|
byte[] wrongMessageLength = Hex.decode("000000000000000000000000010000020000000000");
|
||||||
bais = new ByteArrayInputStream(wrongMessageLength);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bitcoinPacketHeader = new BitcoinSerializer.BitcoinPacketHeader(bais);
|
new BitcoinSerializer.BitcoinPacketHeader(ByteBuffer.wrap(wrongMessageLength));
|
||||||
fail();
|
fail();
|
||||||
} catch (ProtocolException e) {
|
} catch (ProtocolException e) {
|
||||||
// expected
|
// expected
|
||||||
} catch (IOException e) {
|
|
||||||
fail();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSeekPastMagicBytes() {
|
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.
|
// Fail in another way, there is data in the stream but no magic bytes.
|
||||||
byte[] brokenMessage = Hex.decode("000000");
|
byte[] brokenMessage = Hex.decode("000000");
|
||||||
bais = new ByteArrayInputStream(brokenMessage);
|
|
||||||
try {
|
try {
|
||||||
bs.seekPastMagicBytes(bais);
|
new BitcoinSerializer(MainNetParams.get()).seekPastMagicBytes(ByteBuffer.wrap(brokenMessage));
|
||||||
fail();
|
fail();
|
||||||
} catch (IOException e) {
|
} catch (BufferUnderflowException e) {
|
||||||
// expected
|
// 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.params.UnitTestParams;
|
||||||
import com.google.bitcoin.store.MemoryBlockStore;
|
import com.google.bitcoin.store.MemoryBlockStore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
import org.spongycastle.util.encoders.Hex;
|
import org.spongycastle.util.encoders.Hex;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
@RunWith(value = Parameterized.class)
|
||||||
public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
|
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
|
@Test
|
||||||
// Simple deserialization sanity check
|
// Simple deserialization sanity check
|
||||||
public void deserializeFilteredBlock() throws Exception {
|
public void deserializeFilteredBlock() throws Exception {
|
||||||
@@ -87,10 +101,10 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
|
|||||||
peerGroup.addWallet(wallet);
|
peerGroup.addWallet(wallet);
|
||||||
blockChain.addWallet(wallet);
|
blockChain.addWallet(wallet);
|
||||||
|
|
||||||
peerGroup.start();
|
peerGroup.startAndWait();
|
||||||
|
|
||||||
// Create a peer.
|
// Create a peer.
|
||||||
FakeChannel p1 = connectPeer(1);
|
InboundMessageQueuer p1 = connectPeer(1);
|
||||||
assertEquals(1, peerGroup.numConnectedPeers());
|
assertEquals(1, peerGroup.numConnectedPeers());
|
||||||
// Send an inv for block 100001
|
// Send an inv for block 100001
|
||||||
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||||
@@ -115,7 +129,9 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
|
|||||||
inbound(p1, tx2);
|
inbound(p1, tx2);
|
||||||
inbound(p1, tx3);
|
inbound(p1, tx3);
|
||||||
inbound(p1, new Pong(((Ping)ping).getNonce()));
|
inbound(p1, new Pong(((Ping)ping).getNonce()));
|
||||||
|
|
||||||
|
pingAndWait(p1);
|
||||||
|
|
||||||
Set<Transaction> transactions = wallet.getTransactions(false);
|
Set<Transaction> transactions = wallet.getTransactions(false);
|
||||||
assertTrue(transactions.size() == 4);
|
assertTrue(transactions.size() == 4);
|
||||||
for (Transaction tx : transactions) {
|
for (Transaction tx : transactions) {
|
||||||
@@ -128,5 +144,6 @@ public class FilteredBlockAndPartialMerkleTreeTests extends TestWithPeerGroup {
|
|||||||
// Peer 1 goes away.
|
// Peer 1 goes away.
|
||||||
closePeer(peerOf(p1));
|
closePeer(peerOf(p1));
|
||||||
peerGroup.stop();
|
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.junit.Test;
|
||||||
import org.spongycastle.util.encoders.Hex;
|
import org.spongycastle.util.encoders.Hex;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static com.google.bitcoin.utils.TestUtils.createFakeBlock;
|
import static com.google.bitcoin.utils.TestUtils.createFakeBlock;
|
||||||
@@ -179,8 +179,8 @@ public class LazyParseByteCacheTest {
|
|||||||
BitcoinSerializer bs = new BitcoinSerializer(unitTestParams, lazy, retain);
|
BitcoinSerializer bs = new BitcoinSerializer(unitTestParams, lazy, retain);
|
||||||
Block b1;
|
Block b1;
|
||||||
Block bRef;
|
Block bRef;
|
||||||
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
|
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
|
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
|
|
||||||
//verify our reference BitcoinSerializer produces matching byte array.
|
//verify our reference BitcoinSerializer produces matching byte array.
|
||||||
bos.reset();
|
bos.reset();
|
||||||
@@ -231,8 +231,8 @@ public class LazyParseByteCacheTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//refresh block
|
//refresh block
|
||||||
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
|
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
|
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
|
|
||||||
//retrieve a value from header
|
//retrieve a value from header
|
||||||
b1.getDifficultyTarget();
|
b1.getDifficultyTarget();
|
||||||
@@ -244,8 +244,8 @@ public class LazyParseByteCacheTest {
|
|||||||
|
|
||||||
|
|
||||||
//refresh block
|
//refresh block
|
||||||
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
|
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
|
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
|
|
||||||
//retrieve a value from a child and header
|
//retrieve a value from a child and header
|
||||||
b1.getDifficultyTarget();
|
b1.getDifficultyTarget();
|
||||||
@@ -270,8 +270,8 @@ public class LazyParseByteCacheTest {
|
|||||||
serDeser(bs, b1, bos.toByteArray(), null, null);
|
serDeser(bs, b1, bos.toByteArray(), null, null);
|
||||||
|
|
||||||
//refresh block
|
//refresh block
|
||||||
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
|
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
|
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
|
|
||||||
//change a value in header
|
//change a value in header
|
||||||
b1.setNonce(23);
|
b1.setNonce(23);
|
||||||
@@ -289,8 +289,8 @@ public class LazyParseByteCacheTest {
|
|||||||
serDeser(bs, b1, bos.toByteArray(), null, null);
|
serDeser(bs, b1, bos.toByteArray(), null, null);
|
||||||
|
|
||||||
//refresh block
|
//refresh block
|
||||||
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
|
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
|
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
|
|
||||||
//retrieve a value from a child of a child
|
//retrieve a value from a child of a child
|
||||||
b1.getTransactions();
|
b1.getTransactions();
|
||||||
@@ -313,8 +313,8 @@ public class LazyParseByteCacheTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//refresh block
|
//refresh block
|
||||||
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
|
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
|
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
|
|
||||||
//add an input
|
//add an input
|
||||||
b1.getTransactions();
|
b1.getTransactions();
|
||||||
@@ -357,10 +357,10 @@ public class LazyParseByteCacheTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//refresh block
|
//refresh block
|
||||||
b1 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
|
b1 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
Block b2 = (Block) bs.deserialize(new ByteArrayInputStream(blockBytes));
|
Block b2 = (Block) bs.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
|
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
Block bRef2 = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
|
Block bRef2 = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
|
|
||||||
//reparent an input
|
//reparent an input
|
||||||
b1.getTransactions();
|
b1.getTransactions();
|
||||||
@@ -397,7 +397,7 @@ public class LazyParseByteCacheTest {
|
|||||||
serDeser(bs, b1, bos.toByteArray(), null, null);
|
serDeser(bs, b1, bos.toByteArray(), null, null);
|
||||||
|
|
||||||
//how about if we refresh it?
|
//how about if we refresh it?
|
||||||
bRef = (Block) bsRef.deserialize(new ByteArrayInputStream(blockBytes));
|
bRef = (Block) bsRef.deserialize(ByteBuffer.wrap(blockBytes));
|
||||||
bos.reset();
|
bos.reset();
|
||||||
bsRef.serialize(bRef, bos);
|
bsRef.serialize(bRef, bos);
|
||||||
serDeser(bs, b1, bos.toByteArray(), null, null);
|
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 {
|
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
|
//reference serializer to produce comparison serialization output after changes to
|
||||||
//message structure.
|
//message structure.
|
||||||
BitcoinSerializer bsRef = new BitcoinSerializer(params, false, false);
|
BitcoinSerializer bsRef = new BitcoinSerializer(params, false, false);
|
||||||
@@ -415,8 +415,8 @@ public class LazyParseByteCacheTest {
|
|||||||
BitcoinSerializer bs = new BitcoinSerializer(params, lazy, retain);
|
BitcoinSerializer bs = new BitcoinSerializer(params, lazy, retain);
|
||||||
Transaction t1;
|
Transaction t1;
|
||||||
Transaction tRef;
|
Transaction tRef;
|
||||||
t1 = (Transaction) bs.deserialize(new ByteArrayInputStream(txBytes));
|
t1 = (Transaction) bs.deserialize(ByteBuffer.wrap(txBytes));
|
||||||
tRef = (Transaction) bsRef.deserialize(new ByteArrayInputStream(txBytes));
|
tRef = (Transaction) bsRef.deserialize(ByteBuffer.wrap(txBytes));
|
||||||
|
|
||||||
//verify our reference BitcoinSerializer produces matching byte array.
|
//verify our reference BitcoinSerializer produces matching byte array.
|
||||||
bos.reset();
|
bos.reset();
|
||||||
@@ -454,8 +454,8 @@ public class LazyParseByteCacheTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//refresh tx
|
//refresh tx
|
||||||
t1 = (Transaction) bs.deserialize(new ByteArrayInputStream(txBytes));
|
t1 = (Transaction) bs.deserialize(ByteBuffer.wrap(txBytes));
|
||||||
tRef = (Transaction) bsRef.deserialize(new ByteArrayInputStream(txBytes));
|
tRef = (Transaction) bsRef.deserialize(ByteBuffer.wrap(txBytes));
|
||||||
|
|
||||||
//add an input
|
//add an input
|
||||||
if (t1.getInputs().size() > 0) {
|
if (t1.getInputs().size() > 0) {
|
||||||
@@ -482,7 +482,7 @@ public class LazyParseByteCacheTest {
|
|||||||
bs.serialize(message, bos);
|
bs.serialize(message, bos);
|
||||||
byte[] b1 = bos.toByteArray();
|
byte[] b1 = bos.toByteArray();
|
||||||
|
|
||||||
Message m2 = bs.deserialize(new ByteArrayInputStream(b1));
|
Message m2 = bs.deserialize(ByteBuffer.wrap(b1));
|
||||||
|
|
||||||
assertEquals(message, m2);
|
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.store.MemoryBlockStore;
|
||||||
import com.google.bitcoin.utils.TestUtils;
|
import com.google.bitcoin.utils.TestUtils;
|
||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.HashSet;
|
import java.util.*;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
|
||||||
// TX announcement and broadcast is tested in TransactionBroadcastTest.
|
// TX announcement and broadcast is tested in TransactionBroadcastTest.
|
||||||
|
|
||||||
|
@RunWith(value = Parameterized.class)
|
||||||
public class PeerGroupTest extends TestWithPeerGroup {
|
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
|
@Override
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
@@ -54,10 +70,62 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void listener() throws Exception {
|
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() {
|
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);
|
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));
|
assertTrue(peerGroup.removeEventListener(listener));
|
||||||
|
assertFalse(peerGroup.removeEventListener(listener));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -94,8 +162,8 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
peerGroup.startAndWait();
|
peerGroup.startAndWait();
|
||||||
|
|
||||||
// Create a couple of peers.
|
// Create a couple of peers.
|
||||||
FakeChannel p1 = connectPeer(1);
|
InboundMessageQueuer p1 = connectPeer(1);
|
||||||
FakeChannel p2 = connectPeer(2);
|
InboundMessageQueuer p2 = connectPeer(2);
|
||||||
|
|
||||||
// Check the peer accessors.
|
// Check the peer accessors.
|
||||||
assertEquals(2, peerGroup.numConnectedPeers());
|
assertEquals(2, peerGroup.numConnectedPeers());
|
||||||
@@ -122,6 +190,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
GetDataMessage getdata = (GetDataMessage) outbound(p2);
|
GetDataMessage getdata = (GetDataMessage) outbound(p2);
|
||||||
assertNotNull(getdata);
|
assertNotNull(getdata);
|
||||||
inbound(p2, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
inbound(p2, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
||||||
|
pingAndWait(p2);
|
||||||
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||||
peerGroup.stopAndWait();
|
peerGroup.stopAndWait();
|
||||||
}
|
}
|
||||||
@@ -132,8 +201,8 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
peerGroup.startAndWait();
|
peerGroup.startAndWait();
|
||||||
|
|
||||||
// Create a couple of peers.
|
// Create a couple of peers.
|
||||||
FakeChannel p1 = connectPeer(1);
|
InboundMessageQueuer p1 = connectPeer(1);
|
||||||
FakeChannel p2 = connectPeer(2);
|
InboundMessageQueuer p2 = connectPeer(2);
|
||||||
assertEquals(2, peerGroup.numConnectedPeers());
|
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
|
// 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);
|
inv.addBlock(b3);
|
||||||
// Only peer 1 tries to download it.
|
// Only peer 1 tries to download it.
|
||||||
inbound(p1, inv);
|
inbound(p1, inv);
|
||||||
|
pingAndWait(p1);
|
||||||
|
|
||||||
assertTrue(outbound(p1) instanceof GetDataMessage);
|
assertTrue(outbound(p1) instanceof GetDataMessage);
|
||||||
assertNull(outbound(p2));
|
assertNull(outbound(p2));
|
||||||
// Peer 1 goes away, peer 2 becomes the download peer and thus queries the remote mempool.
|
// 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));
|
closePeer(peerOf(p1));
|
||||||
|
p1CloseFuture.get();
|
||||||
// Peer 2 fetches it next time it hears an inv (should it fetch immediately?).
|
// Peer 2 fetches it next time it hears an inv (should it fetch immediately?).
|
||||||
inbound(p2, inv);
|
inbound(p2, inv);
|
||||||
assertTrue(outbound(p2) instanceof GetDataMessage);
|
assertTrue(outbound(p2) instanceof GetDataMessage);
|
||||||
@@ -167,7 +245,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
peerGroup.startAndWait();
|
peerGroup.startAndWait();
|
||||||
|
|
||||||
// Create a couple of peers.
|
// Create a couple of peers.
|
||||||
FakeChannel p1 = connectPeer(1);
|
InboundMessageQueuer p1 = connectPeer(1);
|
||||||
|
|
||||||
// Set up a little block chain.
|
// Set up a little block chain.
|
||||||
Block b1 = TestUtils.createFakeBlock(blockStore).block;
|
Block b1 = TestUtils.createFakeBlock(blockStore).block;
|
||||||
@@ -190,7 +268,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
// We hand back the first block.
|
// We hand back the first block.
|
||||||
inbound(p1, b1);
|
inbound(p1, b1);
|
||||||
// Now we successfully connect to another peer. There should be no messages sent.
|
// 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);
|
Message message = (Message)outbound(p2);
|
||||||
assertNull(message == null ? "" : message.toString(), message);
|
assertNull(message == null ? "" : message.toString(), message);
|
||||||
peerGroup.stop();
|
peerGroup.stop();
|
||||||
@@ -200,6 +278,8 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
public void transactionConfidence() throws Exception {
|
public void transactionConfidence() throws Exception {
|
||||||
// Checks that we correctly count how many peers broadcast a transaction, so we can establish some measure of
|
// 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.
|
// its trustworthyness assuming an untampered with internet connection.
|
||||||
|
peerGroup.startAndWait();
|
||||||
|
|
||||||
final Transaction[] event = new Transaction[2];
|
final Transaction[] event = new Transaction[2];
|
||||||
peerGroup.addEventListener(new AbstractPeerEventListener() {
|
peerGroup.addEventListener(new AbstractPeerEventListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -208,9 +288,9 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
}
|
}
|
||||||
}, Threading.SAME_THREAD);
|
}, Threading.SAME_THREAD);
|
||||||
|
|
||||||
FakeChannel p1 = connectPeer(1);
|
InboundMessageQueuer p1 = connectPeer(1);
|
||||||
FakeChannel p2 = connectPeer(2);
|
InboundMessageQueuer p2 = connectPeer(2);
|
||||||
FakeChannel p3 = connectPeer(3);
|
InboundMessageQueuer p3 = connectPeer(3);
|
||||||
|
|
||||||
Transaction tx = TestUtils.createFakeTx(params, Utils.toNanoCoins(20, 0), address);
|
Transaction tx = TestUtils.createFakeTx(params, Utils.toNanoCoins(20, 0), address);
|
||||||
InventoryMessage inv = new InventoryMessage(params);
|
InventoryMessage inv = new InventoryMessage(params);
|
||||||
@@ -247,6 +327,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
});
|
});
|
||||||
// A straggler reports in.
|
// A straggler reports in.
|
||||||
inbound(p3, inv);
|
inbound(p3, inv);
|
||||||
|
pingAndWait(p3);
|
||||||
Threading.waitForUserCode();
|
Threading.waitForUserCode();
|
||||||
assertEquals(tx, event[1]);
|
assertEquals(tx, event[1]);
|
||||||
assertEquals(3, tx.getConfidence().numBroadcastPeers());
|
assertEquals(3, tx.getConfidence().numBroadcastPeers());
|
||||||
@@ -280,8 +361,10 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
peerGroup.startAndWait();
|
peerGroup.startAndWait();
|
||||||
peerGroup.setPingIntervalMsec(0);
|
peerGroup.setPingIntervalMsec(0);
|
||||||
VersionMessage versionMessage = new VersionMessage(params, 2);
|
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);
|
connectPeer(1, versionMessage);
|
||||||
|
peerGroup.waitForPeers(1).get();
|
||||||
assertFalse(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE);
|
assertFalse(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,10 +373,12 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
peerGroup.startAndWait();
|
peerGroup.startAndWait();
|
||||||
peerGroup.setPingIntervalMsec(100);
|
peerGroup.setPingIntervalMsec(100);
|
||||||
VersionMessage versionMessage = new VersionMessage(params, 2);
|
VersionMessage versionMessage = new VersionMessage(params, 2);
|
||||||
versionMessage.clientVersion = Pong.MIN_PROTOCOL_VERSION;
|
versionMessage.clientVersion = FilteredBlock.MIN_PROTOCOL_VERSION;
|
||||||
FakeChannel p1 = connectPeer(1, versionMessage);
|
versionMessage.localServices = VersionMessage.NODE_NETWORK;
|
||||||
|
InboundMessageQueuer p1 = connectPeer(1, versionMessage);
|
||||||
Ping ping = (Ping) outbound(p1);
|
Ping ping = (Ping) outbound(p1);
|
||||||
inbound(p1, new Pong(ping.getNonce()));
|
inbound(p1, new Pong(ping.getNonce()));
|
||||||
|
pingAndWait(p1);
|
||||||
assertTrue(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE);
|
assertTrue(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE);
|
||||||
// The call to outbound should block until a ping arrives.
|
// The call to outbound should block until a ping arrives.
|
||||||
ping = (Ping) waitForOutbound(p1);
|
ping = (Ping) waitForOutbound(p1);
|
||||||
@@ -305,26 +390,53 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
public void downloadPeerSelection() throws Exception {
|
public void downloadPeerSelection() throws Exception {
|
||||||
peerGroup.startAndWait();
|
peerGroup.startAndWait();
|
||||||
VersionMessage versionMessage2 = new VersionMessage(params, 2);
|
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);
|
VersionMessage versionMessage3 = new VersionMessage(params, 3);
|
||||||
versionMessage3.clientVersion = 60000;
|
versionMessage3.clientVersion = FilteredBlock.MIN_PROTOCOL_VERSION;
|
||||||
|
versionMessage3.localServices = VersionMessage.NODE_NETWORK;
|
||||||
assertNull(peerGroup.getDownloadPeer());
|
assertNull(peerGroup.getDownloadPeer());
|
||||||
Peer a = PeerGroup.peerFromChannel(connectPeer(1, versionMessage2));
|
Peer a = connectPeer(1, versionMessage2).peer;
|
||||||
assertEquals(2, peerGroup.getMostCommonChainHeight());
|
assertEquals(2, peerGroup.getMostCommonChainHeight());
|
||||||
assertEquals(a, peerGroup.getDownloadPeer());
|
assertEquals(a, peerGroup.getDownloadPeer());
|
||||||
PeerGroup.peerFromChannel(connectPeer(2, versionMessage2));
|
connectPeer(2, versionMessage2);
|
||||||
assertEquals(2, peerGroup.getMostCommonChainHeight());
|
assertEquals(2, peerGroup.getMostCommonChainHeight());
|
||||||
assertEquals(a, peerGroup.getDownloadPeer()); // No change.
|
assertEquals(a, peerGroup.getDownloadPeer()); // No change.
|
||||||
Peer c = PeerGroup.peerFromChannel(connectPeer(3, versionMessage3));
|
Peer c = connectPeer(3, versionMessage3).peer;
|
||||||
assertEquals(2, peerGroup.getMostCommonChainHeight());
|
assertEquals(2, peerGroup.getMostCommonChainHeight());
|
||||||
assertEquals(a, peerGroup.getDownloadPeer()); // No change yet.
|
assertEquals(a, peerGroup.getDownloadPeer()); // No change yet.
|
||||||
PeerGroup.peerFromChannel(connectPeer(4, versionMessage3));
|
connectPeer(4, versionMessage3);
|
||||||
assertEquals(3, peerGroup.getMostCommonChainHeight());
|
assertEquals(3, peerGroup.getMostCommonChainHeight());
|
||||||
assertEquals(c, peerGroup.getDownloadPeer()); // Switch to first peer advertising new height.
|
assertEquals(c, peerGroup.getDownloadPeer()); // Switch to first peer advertising new height.
|
||||||
// New peer with a higher protocol version but same chain 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;
|
versionMessage4.clientVersion = 100000;
|
||||||
Peer d = PeerGroup.peerFromChannel(connectPeer(5, versionMessage4));
|
versionMessage4.localServices = VersionMessage.NODE_NETWORK;
|
||||||
assertEquals(d, peerGroup.getDownloadPeer());
|
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;
|
package com.google.bitcoin.core;
|
||||||
|
|
||||||
import com.google.bitcoin.core.Peer.PeerHandler;
|
|
||||||
import com.google.bitcoin.params.TestNet3Params;
|
import com.google.bitcoin.params.TestNet3Params;
|
||||||
import com.google.bitcoin.utils.TestUtils;
|
import com.google.bitcoin.utils.TestUtils;
|
||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import org.easymock.Capture;
|
import org.junit.After;
|
||||||
import org.easymock.CaptureType;
|
|
||||||
import org.jboss.netty.channel.*;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.SocketException;
|
||||||
import java.net.Socket;
|
import java.nio.channels.ClosedChannelException;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
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 com.google.bitcoin.utils.TestUtils.*;
|
||||||
import static org.easymock.EasyMock.*;
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
@RunWith(value = Parameterized.class)
|
||||||
public class PeerTest extends TestWithNetworkConnections {
|
public class PeerTest extends TestWithNetworkConnections {
|
||||||
private Peer peer;
|
private Peer peer;
|
||||||
private Capture<DownstreamMessageEvent> event;
|
private InboundMessageQueuer writeTarget;
|
||||||
private PeerHandler handler;
|
|
||||||
private static final int OTHER_PEER_CHAIN_HEIGHT = 110;
|
private static final int OTHER_PEER_CHAIN_HEIGHT = 110;
|
||||||
private MemoryPool memoryPool;
|
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
|
@Override
|
||||||
@Before
|
@Before
|
||||||
@@ -62,62 +76,35 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
|
|
||||||
memoryPool = new MemoryPool();
|
memoryPool = new MemoryPool();
|
||||||
VersionMessage ver = new VersionMessage(unitTestParams, 100);
|
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);
|
peer.addWallet(wallet);
|
||||||
handler = peer.getHandler();
|
}
|
||||||
event = new Capture<DownstreamMessageEvent>(CaptureType.ALL);
|
|
||||||
pipeline.sendDownstream(capture(event));
|
@After
|
||||||
expectLastCall().anyTimes();
|
public void tearDown() throws Exception {
|
||||||
|
super.tearDown();
|
||||||
|
assertFalse(fail.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void connect() throws Exception {
|
private void connect() throws Exception {
|
||||||
connect(handler, channel, ctx, 70001);
|
connectWithVersion(70001);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void connectWithVersion(int version) throws Exception {
|
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);
|
VersionMessage peerVersion = new VersionMessage(unitTestParams, OTHER_PEER_CHAIN_HEIGHT);
|
||||||
peerVersion.clientVersion = version;
|
peerVersion.clientVersion = version;
|
||||||
DownstreamMessageEvent versionEvent =
|
peerVersion.localServices = VersionMessage.NODE_NETWORK;
|
||||||
new DownstreamMessageEvent(channel, Channels.future(channel), peerVersion, null);
|
writeTarget = connect(peer, peerVersion);
|
||||||
handler.messageReceived(ctx, versionEvent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAddEventListener() throws Exception {
|
public void testAddEventListener() throws Exception {
|
||||||
control.replay();
|
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
PeerEventListener listener = new AbstractPeerEventListener();
|
PeerEventListener listener = new AbstractPeerEventListener();
|
||||||
peer.addEventListener(listener);
|
peer.addEventListener(listener);
|
||||||
assertTrue(peer.removeEventListener(listener));
|
assertTrue(peer.removeEventListener(listener));
|
||||||
assertFalse(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
|
// Check that it runs through the event loop and shut down correctly
|
||||||
@Test
|
@Test
|
||||||
@@ -135,12 +122,10 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
Block b4 = makeSolvedTestBlock(b3);
|
Block b4 = makeSolvedTestBlock(b3);
|
||||||
Block b5 = makeSolvedTestBlock(b4);
|
Block b5 = makeSolvedTestBlock(b4);
|
||||||
|
|
||||||
control.replay();
|
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
peer.startBlockChainDownload();
|
peer.startBlockChainDownload();
|
||||||
GetBlocksMessage getblocks = (GetBlocksMessage)outbound();
|
GetBlocksMessage getblocks = (GetBlocksMessage)outbound(writeTarget);
|
||||||
assertEquals(blockStore.getChainHead().getHeader().getHash(), getblocks.getLocator().get(0));
|
assertEquals(blockStore.getChainHead().getHeader().getHash(), getblocks.getLocator().get(0));
|
||||||
assertEquals(Sha256Hash.ZERO_HASH, getblocks.getStopHash());
|
assertEquals(Sha256Hash.ZERO_HASH, getblocks.getStopHash());
|
||||||
// Remote peer sends us an inv with some blocks.
|
// Remote peer sends us an inv with some blocks.
|
||||||
@@ -148,27 +133,27 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
inv.addBlock(b2);
|
inv.addBlock(b2);
|
||||||
inv.addBlock(b3);
|
inv.addBlock(b3);
|
||||||
// We do a getdata on them.
|
// We do a getdata on them.
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
GetDataMessage getdata = (GetDataMessage)outbound();
|
GetDataMessage getdata = (GetDataMessage)outbound(writeTarget);
|
||||||
assertEquals(b2.getHash(), getdata.getItems().get(0).hash);
|
assertEquals(b2.getHash(), getdata.getItems().get(0).hash);
|
||||||
assertEquals(b3.getHash(), getdata.getItems().get(1).hash);
|
assertEquals(b3.getHash(), getdata.getItems().get(1).hash);
|
||||||
assertEquals(2, getdata.getItems().size());
|
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
|
// 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.
|
// best chain head in it.
|
||||||
inbound(peer, b2);
|
inbound(writeTarget, b2);
|
||||||
inbound(peer, b3);
|
inbound(writeTarget, b3);
|
||||||
|
|
||||||
inv = new InventoryMessage(unitTestParams);
|
inv = new InventoryMessage(unitTestParams);
|
||||||
inv.addBlock(b5);
|
inv.addBlock(b5);
|
||||||
// We request the head block.
|
// We request the head block.
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
getdata = (GetDataMessage)outbound();
|
getdata = (GetDataMessage)outbound(writeTarget);
|
||||||
assertEquals(b5.getHash(), getdata.getItems().get(0).hash);
|
assertEquals(b5.getHash(), getdata.getItems().get(0).hash);
|
||||||
assertEquals(1, getdata.getItems().size());
|
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
|
// Peer sends us the head block. The act of receiving the orphan block triggers a getblocks to fill in the
|
||||||
// rest of the chain.
|
// rest of the chain.
|
||||||
inbound(peer, b5);
|
inbound(writeTarget, b5);
|
||||||
getblocks = (GetBlocksMessage)outbound();
|
getblocks = (GetBlocksMessage)outbound(writeTarget);
|
||||||
assertEquals(b5.getHash(), getblocks.getStopHash());
|
assertEquals(b5.getHash(), getblocks.getStopHash());
|
||||||
assertEquals(b3.getHash(), getblocks.getLocator().get(0));
|
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
|
// 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);
|
Block b6 = makeSolvedTestBlock(b5);
|
||||||
inv = new InventoryMessage(unitTestParams);
|
inv = new InventoryMessage(unitTestParams);
|
||||||
inv.addBlock(b6);
|
inv.addBlock(b6);
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
getdata = (GetDataMessage)outbound();
|
getdata = (GetDataMessage)outbound(writeTarget);
|
||||||
assertEquals(1, getdata.getItems().size());
|
assertEquals(1, getdata.getItems().size());
|
||||||
assertEquals(b6.getHash(), getdata.getItems().get(0).hash);
|
assertEquals(b6.getHash(), getdata.getItems().get(0).hash);
|
||||||
inbound(peer, b6);
|
inbound(writeTarget, b6);
|
||||||
assertFalse(event.hasCaptured()); // Nothing is sent at this point.
|
assertNull(outbound(writeTarget)); // Nothing is sent at this point.
|
||||||
// We're still waiting for the response to the getblocks (b3,b5) sent above.
|
// We're still waiting for the response to the getblocks (b3,b5) sent above.
|
||||||
inv = new InventoryMessage(unitTestParams);
|
inv = new InventoryMessage(unitTestParams);
|
||||||
inv.addBlock(b4);
|
inv.addBlock(b4);
|
||||||
inv.addBlock(b5);
|
inv.addBlock(b5);
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
getdata = (GetDataMessage)outbound();
|
getdata = (GetDataMessage)outbound(writeTarget);
|
||||||
assertEquals(1, getdata.getItems().size());
|
assertEquals(1, getdata.getItems().size());
|
||||||
assertEquals(b4.getHash(), getdata.getItems().get(0).hash);
|
assertEquals(b4.getHash(), getdata.getItems().get(0).hash);
|
||||||
// We already have b5 from before, so it's not requested again.
|
// We already have b5 from before, so it's not requested again.
|
||||||
inbound(peer, b4);
|
inbound(writeTarget, b4);
|
||||||
assertFalse(event.hasCaptured());
|
assertNull(outbound(writeTarget));
|
||||||
// b5 and b6 are now connected by the block chain and we're done.
|
// b5 and b6 are now connected by the block chain and we're done.
|
||||||
|
assertNull(outbound(writeTarget));
|
||||||
closePeer(peer);
|
closePeer(peer);
|
||||||
control.verify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that an inventory tickle is processed correctly when downloading missing blocks is active.
|
// Check that an inventory tickle is processed correctly when downloading missing blocks is active.
|
||||||
@Test
|
@Test
|
||||||
public void invTickle() throws Exception {
|
public void invTickle() throws Exception {
|
||||||
control.replay();
|
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
Block b1 = createFakeBlock(blockStore).block;
|
Block b1 = createFakeBlock(blockStore).block;
|
||||||
@@ -213,20 +196,20 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
// Make a missing block.
|
// Make a missing block.
|
||||||
Block b2 = makeSolvedTestBlock(b1);
|
Block b2 = makeSolvedTestBlock(b1);
|
||||||
Block b3 = makeSolvedTestBlock(b2);
|
Block b3 = makeSolvedTestBlock(b2);
|
||||||
inbound(peer, b3);
|
inbound(writeTarget, b3);
|
||||||
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||||
InventoryItem item = new InventoryItem(InventoryItem.Type.Block, b3.getHash());
|
InventoryItem item = new InventoryItem(InventoryItem.Type.Block, b3.getHash());
|
||||||
inv.addItem(item);
|
inv.addItem(item);
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
|
|
||||||
GetBlocksMessage getblocks = (GetBlocksMessage)outbound();
|
GetBlocksMessage getblocks = (GetBlocksMessage)outbound(writeTarget);
|
||||||
List<Sha256Hash> expectedLocator = new ArrayList<Sha256Hash>();
|
List<Sha256Hash> expectedLocator = new ArrayList<Sha256Hash>();
|
||||||
expectedLocator.add(b1.getHash());
|
expectedLocator.add(b1.getHash());
|
||||||
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
|
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
|
||||||
|
|
||||||
assertEquals(getblocks.getLocator(), expectedLocator);
|
assertEquals(getblocks.getLocator(), expectedLocator);
|
||||||
assertEquals(getblocks.getStopHash(), b3.getHash());
|
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.
|
// 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 {
|
public void invNoDownload() throws Exception {
|
||||||
// Don't download missing blocks.
|
// Don't download missing blocks.
|
||||||
peer.setDownloadData(false);
|
peer.setDownloadData(false);
|
||||||
|
|
||||||
control.replay();
|
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
// Make a missing block that we receive.
|
// Make a missing block that we receive.
|
||||||
@@ -248,16 +229,14 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||||
InventoryItem item = new InventoryItem(InventoryItem.Type.Block, b2.getHash());
|
InventoryItem item = new InventoryItem(InventoryItem.Type.Block, b2.getHash());
|
||||||
inv.addItem(item);
|
inv.addItem(item);
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
|
|
||||||
// Peer does nothing with it.
|
// Peer does nothing with it.
|
||||||
control.verify();
|
assertNull(outbound(writeTarget));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invDownloadTx() throws Exception {
|
public void invDownloadTx() throws Exception {
|
||||||
control.replay();
|
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
peer.setDownloadData(true);
|
peer.setDownloadData(true);
|
||||||
@@ -267,34 +246,31 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||||
InventoryItem item = new InventoryItem(InventoryItem.Type.Transaction, tx.getHash());
|
InventoryItem item = new InventoryItem(InventoryItem.Type.Transaction, tx.getHash());
|
||||||
inv.addItem(item);
|
inv.addItem(item);
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
// Peer hasn't seen it before, so will ask for it.
|
// 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(1, getdata.getItems().size());
|
||||||
assertEquals(tx.getHash(), getdata.getItems().get(0).hash);
|
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).
|
// Ask for the dependency, it's not in the mempool (in chain).
|
||||||
getdata = (GetDataMessage) outbound();
|
getdata = (GetDataMessage) outbound(writeTarget);
|
||||||
inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
inbound(writeTarget, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
||||||
|
pingAndWait(writeTarget);
|
||||||
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invDownloadTxMultiPeer() throws Exception {
|
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.
|
// Check co-ordination of which peer to download via the memory pool.
|
||||||
MockNetworkConnection conn2 = createMockNetworkConnection();
|
|
||||||
VersionMessage ver = new VersionMessage(unitTestParams, 100);
|
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);
|
peer2.addWallet(wallet);
|
||||||
|
VersionMessage peerVersion = new VersionMessage(unitTestParams, OTHER_PEER_CHAIN_HEIGHT);
|
||||||
|
peerVersion.clientVersion = 70001;
|
||||||
|
peerVersion.localServices = VersionMessage.NODE_NETWORK;
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
connect(peer2.getHandler(), channel2, ctx2, 70001);
|
InboundMessageQueuer writeTarget2 = connect(peer2, peerVersion);
|
||||||
|
|
||||||
// Make a tx and advertise it to one of the peers.
|
// Make a tx and advertise it to one of the peers.
|
||||||
BigInteger value = Utils.toNanoCoins(1, 0);
|
BigInteger value = Utils.toNanoCoins(1, 0);
|
||||||
@@ -303,51 +279,74 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
InventoryItem item = new InventoryItem(InventoryItem.Type.Transaction, tx.getHash());
|
InventoryItem item = new InventoryItem(InventoryItem.Type.Transaction, tx.getHash());
|
||||||
inv.addItem(item);
|
inv.addItem(item);
|
||||||
|
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
|
|
||||||
// We got a getdata message.
|
// We got a getdata message.
|
||||||
GetDataMessage message = (GetDataMessage)outbound();
|
GetDataMessage message = (GetDataMessage)outbound(writeTarget);
|
||||||
assertEquals(1, message.getItems().size());
|
assertEquals(1, message.getItems().size());
|
||||||
assertEquals(tx.getHash(), message.getItems().get(0).hash);
|
assertEquals(tx.getHash(), message.getItems().get(0).hash);
|
||||||
assertTrue(memoryPool.maybeWasSeen(tx.getHash()));
|
assertTrue(memoryPool.maybeWasSeen(tx.getHash()));
|
||||||
|
|
||||||
// Advertising to peer2 results in no getdata message.
|
// Advertising to peer2 results in no getdata message.
|
||||||
conn2.inbound(inv);
|
inbound(writeTarget2, inv);
|
||||||
assertFalse(event.hasCaptured());
|
pingAndWait(writeTarget2);
|
||||||
|
assertNull(outbound(writeTarget2));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that inventory message containing blocks we want is processed correctly.
|
// Check that inventory message containing blocks we want is processed correctly.
|
||||||
@Test
|
@Test
|
||||||
public void newBlock() throws Exception {
|
public void newBlock() throws Exception {
|
||||||
PeerEventListener listener = control.createMock(PeerEventListener.class);
|
|
||||||
|
|
||||||
Block b1 = createFakeBlock(blockStore).block;
|
Block b1 = createFakeBlock(blockStore).block;
|
||||||
blockChain.add(b1);
|
blockChain.add(b1);
|
||||||
Block b2 = makeSolvedTestBlock(b1);
|
final Block b2 = makeSolvedTestBlock(b1);
|
||||||
// Receive notification of a new block.
|
// 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());
|
InventoryItem item = new InventoryItem(InventoryItem.Type.Block, b2.getHash());
|
||||||
inv.addItem(item);
|
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();
|
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();
|
long height = peer.getBestHeight();
|
||||||
|
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
|
pingAndWait(writeTarget);
|
||||||
assertEquals(height + 1, peer.getBestHeight());
|
assertEquals(height + 1, peer.getBestHeight());
|
||||||
// Response to the getdata message.
|
// 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();
|
List<InventoryItem> items = getdata.getItems();
|
||||||
assertEquals(1, items.size());
|
assertEquals(1, items.size());
|
||||||
assertEquals(b2.getHash(), items.get(0).hash);
|
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.
|
// Check that it starts downloading the block chain correctly on request.
|
||||||
@Test
|
@Test
|
||||||
public void startBlockChainDownload() throws Exception {
|
public void startBlockChainDownload() throws Exception {
|
||||||
PeerEventListener listener = control.createMock(PeerEventListener.class);
|
|
||||||
|
|
||||||
Block b1 = createFakeBlock(blockStore).block;
|
Block b1 = createFakeBlock(blockStore).block;
|
||||||
blockChain.add(b1);
|
blockChain.add(b1);
|
||||||
Block b2 = makeSolvedTestBlock(b1);
|
Block b2 = makeSolvedTestBlock(b1);
|
||||||
blockChain.add(b2);
|
blockChain.add(b2);
|
||||||
|
|
||||||
listener.onChainDownloadStarted(peer, 108);
|
|
||||||
expectLastCall();
|
|
||||||
|
|
||||||
control.replay();
|
|
||||||
|
|
||||||
connect();
|
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();
|
peer.startBlockChainDownload();
|
||||||
control.verify();
|
|
||||||
|
|
||||||
List<Sha256Hash> expectedLocator = new ArrayList<Sha256Hash>();
|
List<Sha256Hash> expectedLocator = new ArrayList<Sha256Hash>();
|
||||||
expectedLocator.add(b2.getHash());
|
expectedLocator.add(b2.getHash());
|
||||||
expectedLocator.add(b1.getHash());
|
expectedLocator.add(b1.getHash());
|
||||||
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
|
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
|
||||||
|
|
||||||
GetBlocksMessage message = (GetBlocksMessage) event.getValue().getMessage();
|
GetBlocksMessage message = (GetBlocksMessage) outbound(writeTarget);
|
||||||
assertEquals(message.getLocator(), expectedLocator);
|
assertEquals(message.getLocator(), expectedLocator);
|
||||||
assertEquals(Sha256Hash.ZERO_HASH, message.getStopHash());
|
assertEquals(Sha256Hash.ZERO_HASH, message.getStopHash());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getBlock() throws Exception {
|
public void getBlock() throws Exception {
|
||||||
control.replay();
|
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
Block b1 = createFakeBlock(blockStore).block;
|
Block b1 = createFakeBlock(blockStore).block;
|
||||||
@@ -400,19 +395,42 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
Future<Block> resultFuture = peer.getBlock(b3.getHash());
|
Future<Block> resultFuture = peer.getBlock(b3.getHash());
|
||||||
assertFalse(resultFuture.isDone());
|
assertFalse(resultFuture.isDone());
|
||||||
// Peer asks for it.
|
// Peer asks for it.
|
||||||
GetDataMessage message = (GetDataMessage) event.getValue().getMessage();
|
GetDataMessage message = (GetDataMessage) outbound(writeTarget);
|
||||||
assertEquals(message.getItems().get(0).hash, b3.getHash());
|
assertEquals(message.getItems().get(0).hash, b3.getHash());
|
||||||
assertFalse(resultFuture.isDone());
|
assertFalse(resultFuture.isDone());
|
||||||
// Peer receives it.
|
// Peer receives it.
|
||||||
inbound(peer, b3);
|
inbound(writeTarget, b3);
|
||||||
Block b = resultFuture.get();
|
Block b = resultFuture.get();
|
||||||
assertEquals(b, b3);
|
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
|
@Test
|
||||||
public void fastCatchup() throws Exception {
|
public void fastCatchup() throws Exception {
|
||||||
control.replay();
|
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
// Check that blocks before the fast catchup point are retrieved using getheaders, and after using getblocks.
|
// 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.
|
// Request headers until the last 2 blocks.
|
||||||
peer.setDownloadParameters((Utils.now().getTime() / 1000) - (600*2) + 1, false);
|
peer.setDownloadParameters((Utils.now().getTime() / 1000) - (600*2) + 1, false);
|
||||||
peer.startBlockChainDownload();
|
peer.startBlockChainDownload();
|
||||||
GetHeadersMessage getheaders = (GetHeadersMessage) outbound();
|
GetHeadersMessage getheaders = (GetHeadersMessage) outbound(writeTarget);
|
||||||
List<Sha256Hash> expectedLocator = new ArrayList<Sha256Hash>();
|
List<Sha256Hash> expectedLocator = new ArrayList<Sha256Hash>();
|
||||||
expectedLocator.add(b1.getHash());
|
expectedLocator.add(b1.getHash());
|
||||||
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
|
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
|
||||||
@@ -443,36 +461,38 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
expectedLocator.add(b2.getHash());
|
expectedLocator.add(b2.getHash());
|
||||||
expectedLocator.add(b1.getHash());
|
expectedLocator.add(b1.getHash());
|
||||||
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
|
expectedLocator.add(unitTestParams.getGenesisBlock().getHash());
|
||||||
inbound(peer, headers);
|
inbound(writeTarget, headers);
|
||||||
GetBlocksMessage getblocks = (GetBlocksMessage) outbound();
|
GetBlocksMessage getblocks = (GetBlocksMessage) outbound(writeTarget);
|
||||||
assertEquals(expectedLocator, getblocks.getLocator());
|
assertEquals(expectedLocator, getblocks.getLocator());
|
||||||
assertEquals(Sha256Hash.ZERO_HASH, getblocks.getStopHash());
|
assertEquals(Sha256Hash.ZERO_HASH, getblocks.getStopHash());
|
||||||
// We're supposed to get an inv here.
|
// We're supposed to get an inv here.
|
||||||
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||||
inv.addItem(new InventoryItem(InventoryItem.Type.Block, b3.getHash()));
|
inv.addItem(new InventoryItem(InventoryItem.Type.Block, b3.getHash()));
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
GetDataMessage getdata = (GetDataMessage) event.getValue().getMessage();
|
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
|
||||||
assertEquals(b3.getHash(), getdata.getItems().get(0).hash);
|
assertEquals(b3.getHash(), getdata.getItems().get(0).hash);
|
||||||
// All done.
|
// All done.
|
||||||
inbound(peer, b3);
|
inbound(writeTarget, b3);
|
||||||
|
pingAndWait(writeTarget);
|
||||||
|
closePeer(peer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void pingPong() throws Exception {
|
public void pingPong() throws Exception {
|
||||||
control.replay();
|
|
||||||
connect();
|
connect();
|
||||||
Utils.rollMockClock(0);
|
Utils.rollMockClock(0);
|
||||||
// No ping pong happened yet.
|
// No ping pong happened yet.
|
||||||
assertEquals(Long.MAX_VALUE, peer.getLastPingTime());
|
assertEquals(Long.MAX_VALUE, peer.getLastPingTime());
|
||||||
assertEquals(Long.MAX_VALUE, peer.getPingTime());
|
assertEquals(Long.MAX_VALUE, peer.getPingTime());
|
||||||
ListenableFuture<Long> future = peer.ping();
|
ListenableFuture<Long> future = peer.ping();
|
||||||
Ping pingMsg = (Ping) outbound();
|
|
||||||
assertEquals(Long.MAX_VALUE, peer.getLastPingTime());
|
assertEquals(Long.MAX_VALUE, peer.getLastPingTime());
|
||||||
assertEquals(Long.MAX_VALUE, peer.getPingTime());
|
assertEquals(Long.MAX_VALUE, peer.getPingTime());
|
||||||
assertFalse(future.isDone());
|
assertFalse(future.isDone());
|
||||||
|
Ping pingMsg = (Ping) outbound(writeTarget);
|
||||||
Utils.rollMockClock(5);
|
Utils.rollMockClock(5);
|
||||||
// The pong is returned.
|
// The pong is returned.
|
||||||
inbound(peer, new Pong(pingMsg.getNonce()));
|
inbound(writeTarget, new Pong(pingMsg.getNonce()));
|
||||||
|
pingAndWait(writeTarget);
|
||||||
assertTrue(future.isDone());
|
assertTrue(future.isDone());
|
||||||
long elapsed = future.get();
|
long elapsed = future.get();
|
||||||
assertTrue("" + elapsed, elapsed > 1000);
|
assertTrue("" + elapsed, elapsed > 1000);
|
||||||
@@ -480,9 +500,9 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
assertEquals(elapsed, peer.getPingTime());
|
assertEquals(elapsed, peer.getPingTime());
|
||||||
// Do it again and make sure it affects the average.
|
// Do it again and make sure it affects the average.
|
||||||
future = peer.ping();
|
future = peer.ping();
|
||||||
pingMsg = (Ping) outbound();
|
pingMsg = (Ping) outbound(writeTarget);
|
||||||
Utils.rollMockClock(50);
|
Utils.rollMockClock(50);
|
||||||
inbound(peer, new Pong(pingMsg.getNonce()));
|
inbound(writeTarget, new Pong(pingMsg.getNonce()));
|
||||||
elapsed = future.get();
|
elapsed = future.get();
|
||||||
assertEquals(elapsed, peer.getLastPingTime());
|
assertEquals(elapsed, peer.getLastPingTime());
|
||||||
assertEquals(7250, peer.getPingTime());
|
assertEquals(7250, peer.getPingTime());
|
||||||
@@ -500,7 +520,6 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
|
|
||||||
public void recursiveDownload(boolean useNotFound) throws Exception {
|
public void recursiveDownload(boolean useNotFound) throws Exception {
|
||||||
// Using ping or notfound?
|
// Using ping or notfound?
|
||||||
control.replay();
|
|
||||||
connectWithVersion(useNotFound ? 70001 : 60001);
|
connectWithVersion(useNotFound ? 70001 : 60001);
|
||||||
// Check that we can download all dependencies of an unconfirmed relevant transaction from the mempool.
|
// Check that we can download all dependencies of an unconfirmed relevant transaction from the mempool.
|
||||||
ECKey to = new ECKey();
|
ECKey to = new ECKey();
|
||||||
@@ -543,16 +562,18 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
// Announce the first one. Wait for it to be downloaded.
|
// Announce the first one. Wait for it to be downloaded.
|
||||||
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||||
inv.addTransaction(t1);
|
inv.addTransaction(t1);
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
GetDataMessage getdata = (GetDataMessage) outbound();
|
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
|
||||||
|
Threading.waitForUserCode();
|
||||||
assertEquals(t1.getHash(), getdata.getItems().get(0).hash);
|
assertEquals(t1.getHash(), getdata.getItems().get(0).hash);
|
||||||
inbound(peer, t1);
|
inbound(writeTarget, t1);
|
||||||
|
pingAndWait(writeTarget);
|
||||||
assertEquals(t1, onTx[0]);
|
assertEquals(t1, onTx[0]);
|
||||||
// We want its dependencies so ask for them.
|
// We want its dependencies so ask for them.
|
||||||
ListenableFuture<List<Transaction>> futures = peer.downloadDependencies(t1);
|
ListenableFuture<List<Transaction>> futures = peer.downloadDependencies(t1);
|
||||||
assertFalse(futures.isDone());
|
assertFalse(futures.isDone());
|
||||||
// It will recursively ask for the dependencies of t1: t2, t3, someHash and anotherHash.
|
// 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(4, getdata.getItems().size());
|
||||||
assertEquals(t2.getHash(), getdata.getItems().get(0).hash);
|
assertEquals(t2.getHash(), getdata.getItems().get(0).hash);
|
||||||
assertEquals(t3.getHash(), getdata.getItems().get(1).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);
|
assertEquals(anotherHash, getdata.getItems().get(3).hash);
|
||||||
long nonce = -1;
|
long nonce = -1;
|
||||||
if (!useNotFound)
|
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
|
// 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
|
// false positive. We do this to check that the mempool is being checked for seen transactions before
|
||||||
// requesting them.
|
// requesting them.
|
||||||
inbound(peer, t4);
|
inbound(writeTarget, t4);
|
||||||
// Deliver the requested transactions.
|
// Deliver the requested transactions.
|
||||||
inbound(peer, t2);
|
inbound(writeTarget, t2);
|
||||||
inbound(peer, t3);
|
inbound(writeTarget, t3);
|
||||||
if (useNotFound) {
|
if (useNotFound) {
|
||||||
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
|
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
|
||||||
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, someHash));
|
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, someHash));
|
||||||
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, anotherHash));
|
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, anotherHash));
|
||||||
inbound(peer, notFound);
|
inbound(writeTarget, notFound);
|
||||||
} else {
|
} else {
|
||||||
inbound(peer, new Pong(nonce));
|
inbound(writeTarget, new Pong(nonce));
|
||||||
}
|
}
|
||||||
assertFalse(futures.isDone());
|
assertFalse(futures.isDone());
|
||||||
// It will recursively ask for the dependencies of t2: t5 and t4, but not t3 because it already found t4.
|
// 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());
|
assertEquals(getdata.getItems().get(0).hash, t2.getInput(0).getOutpoint().getHash());
|
||||||
// t5 isn't found and t4 is.
|
// t5 isn't found and t4 is.
|
||||||
if (useNotFound) {
|
if (useNotFound) {
|
||||||
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
|
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
|
||||||
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t5));
|
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t5));
|
||||||
inbound(peer, notFound);
|
inbound(writeTarget, notFound);
|
||||||
} else {
|
} else {
|
||||||
bouncePing();
|
bouncePing();
|
||||||
}
|
}
|
||||||
assertFalse(futures.isDone());
|
assertFalse(futures.isDone());
|
||||||
// Continue to explore the t4 branch and ask for t6, which is in the chain.
|
// 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);
|
assertEquals(t6, getdata.getItems().get(0).hash);
|
||||||
if (useNotFound) {
|
if (useNotFound) {
|
||||||
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
|
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
|
||||||
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t6));
|
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t6));
|
||||||
inbound(peer, notFound);
|
inbound(writeTarget, notFound);
|
||||||
} else {
|
} else {
|
||||||
bouncePing();
|
bouncePing();
|
||||||
}
|
}
|
||||||
|
pingAndWait(writeTarget);
|
||||||
// That's it, we explored the entire tree.
|
// That's it, we explored the entire tree.
|
||||||
assertTrue(futures.isDone());
|
assertTrue(futures.isDone());
|
||||||
List<Transaction> results = futures.get();
|
List<Transaction> results = futures.get();
|
||||||
@@ -608,8 +630,8 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void bouncePing() throws Exception {
|
private void bouncePing() throws Exception {
|
||||||
Ping ping = (Ping) outbound();
|
Ping ping = (Ping) outbound(writeTarget);
|
||||||
inbound(peer, new Pong(ping.getNonce()));
|
inbound(writeTarget, new Pong(ping.getNonce()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -623,7 +645,6 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void timeLockedTransaction(boolean useNotFound) throws Exception {
|
public void timeLockedTransaction(boolean useNotFound) throws Exception {
|
||||||
control.replay();
|
|
||||||
connectWithVersion(useNotFound ? 70001 : 60001);
|
connectWithVersion(useNotFound ? 70001 : 60001);
|
||||||
// Test that if we receive a relevant transaction that has a lock time, it doesn't result in a notification
|
// 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.
|
// 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.
|
// Send a normal relevant transaction, it's received correctly.
|
||||||
Transaction t1 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), key);
|
Transaction t1 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), key);
|
||||||
inbound(peer, t1);
|
inbound(writeTarget, t1);
|
||||||
GetDataMessage getdata = (GetDataMessage) outbound();
|
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
|
||||||
if (useNotFound) {
|
if (useNotFound) {
|
||||||
inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
inbound(writeTarget, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
||||||
} else {
|
} else {
|
||||||
bouncePing();
|
bouncePing();
|
||||||
}
|
}
|
||||||
|
pingAndWait(writeTarget);
|
||||||
Threading.waitForUserCode();
|
Threading.waitForUserCode();
|
||||||
assertNotNull(vtx[0]);
|
assertNotNull(vtx[0]);
|
||||||
vtx[0] = null;
|
vtx[0] = null;
|
||||||
// Send a timelocked transaction, nothing happens.
|
// Send a timelocked transaction, nothing happens.
|
||||||
Transaction t2 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(2, 0), key);
|
Transaction t2 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(2, 0), key);
|
||||||
t2.setLockTime(999999);
|
t2.setLockTime(999999);
|
||||||
inbound(peer, t2);
|
inbound(writeTarget, t2);
|
||||||
Threading.waitForUserCode();
|
Threading.waitForUserCode();
|
||||||
assertNull(vtx[0]);
|
assertNull(vtx[0]);
|
||||||
// Now we want to hear about them. Send another, we are told about it.
|
// Now we want to hear about them. Send another, we are told about it.
|
||||||
wallet.setAcceptRiskyTransactions(true);
|
wallet.setAcceptRiskyTransactions(true);
|
||||||
inbound(peer, t2);
|
inbound(writeTarget, t2);
|
||||||
getdata = (GetDataMessage) outbound();
|
getdata = (GetDataMessage) outbound(writeTarget);
|
||||||
if (useNotFound) {
|
if (useNotFound) {
|
||||||
inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
inbound(writeTarget, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
||||||
} else {
|
} else {
|
||||||
bouncePing();
|
bouncePing();
|
||||||
}
|
}
|
||||||
|
pingAndWait(writeTarget);
|
||||||
Threading.waitForUserCode();
|
Threading.waitForUserCode();
|
||||||
assertEquals(t2, vtx[0]);
|
assertEquals(t2, vtx[0]);
|
||||||
}
|
}
|
||||||
@@ -697,7 +720,6 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
|
|
||||||
private void checkTimeLockedDependency(boolean shouldAccept, boolean useNotFound) throws Exception {
|
private void checkTimeLockedDependency(boolean shouldAccept, boolean useNotFound) throws Exception {
|
||||||
// Initial setup.
|
// Initial setup.
|
||||||
control.replay();
|
|
||||||
connectWithVersion(useNotFound ? 70001 : 60001);
|
connectWithVersion(useNotFound ? 70001 : 60001);
|
||||||
ECKey key = new ECKey();
|
ECKey key = new ECKey();
|
||||||
Wallet wallet = new Wallet(unitTestParams);
|
Wallet wallet = new Wallet(unitTestParams);
|
||||||
@@ -725,30 +747,31 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
// Announce t1.
|
// Announce t1.
|
||||||
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||||
inv.addTransaction(t1);
|
inv.addTransaction(t1);
|
||||||
inbound(peer, inv);
|
inbound(writeTarget, inv);
|
||||||
// Send it.
|
// Send it.
|
||||||
GetDataMessage getdata = (GetDataMessage) outbound();
|
GetDataMessage getdata = (GetDataMessage) outbound(writeTarget);
|
||||||
assertEquals(t1.getHash(), getdata.getItems().get(0).hash);
|
assertEquals(t1.getHash(), getdata.getItems().get(0).hash);
|
||||||
inbound(peer, t1);
|
inbound(writeTarget, t1);
|
||||||
// Nothing arrived at our event listener yet.
|
// Nothing arrived at our event listener yet.
|
||||||
assertNull(vtx[0]);
|
assertNull(vtx[0]);
|
||||||
// We request t2.
|
// We request t2.
|
||||||
getdata = (GetDataMessage) outbound();
|
getdata = (GetDataMessage) outbound(writeTarget);
|
||||||
assertEquals(t2.getHash(), getdata.getItems().get(0).hash);
|
assertEquals(t2.getHash(), getdata.getItems().get(0).hash);
|
||||||
inbound(peer, t2);
|
inbound(writeTarget, t2);
|
||||||
if (!useNotFound)
|
if (!useNotFound)
|
||||||
bouncePing();
|
bouncePing();
|
||||||
// We request t3.
|
// We request t3.
|
||||||
getdata = (GetDataMessage) outbound();
|
getdata = (GetDataMessage) outbound(writeTarget);
|
||||||
assertEquals(t3, getdata.getItems().get(0).hash);
|
assertEquals(t3, getdata.getItems().get(0).hash);
|
||||||
// Can't find it: bottom of tree.
|
// Can't find it: bottom of tree.
|
||||||
if (useNotFound) {
|
if (useNotFound) {
|
||||||
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
|
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
|
||||||
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t3));
|
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t3));
|
||||||
inbound(peer, notFound);
|
inbound(writeTarget, notFound);
|
||||||
} else {
|
} else {
|
||||||
bouncePing();
|
bouncePing();
|
||||||
}
|
}
|
||||||
|
pingAndWait(writeTarget);
|
||||||
Threading.waitForUserCode();
|
Threading.waitForUserCode();
|
||||||
// We're done but still not notified because it was timelocked.
|
// We're done but still not notified because it was timelocked.
|
||||||
if (shouldAccept)
|
if (shouldAccept)
|
||||||
@@ -759,29 +782,51 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void disconnectOldVersions1() throws Exception {
|
public void disconnectOldVersions1() throws Exception {
|
||||||
expect(channel.close()).andReturn(null);
|
|
||||||
control.replay();
|
|
||||||
// Set up the connection with an old version.
|
// Set up the connection with an old version.
|
||||||
handler.connectRequested(ctx, new UpstreamChannelStateEvent(channel, ChannelState.CONNECTED, socketAddress));
|
final SettableFuture<Void> connectedFuture = SettableFuture.create();
|
||||||
VersionMessage peerVersion = new VersionMessage(unitTestParams, OTHER_PEER_CHAIN_HEIGHT);
|
final SettableFuture<Void> disconnectedFuture = SettableFuture.create();
|
||||||
peerVersion.clientVersion = 500;
|
peer.addEventListener(new AbstractPeerEventListener() {
|
||||||
DownstreamMessageEvent versionEvent =
|
@Override
|
||||||
new DownstreamMessageEvent(channel, Channels.future(channel), peerVersion, null);
|
public void onPeerConnected(Peer peer, int peerCount) {
|
||||||
handler.messageReceived(ctx, versionEvent);
|
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
|
@Test
|
||||||
public void disconnectOldVersions2() throws Exception {
|
public void disconnectOldVersions2() throws Exception {
|
||||||
expect(channel.close()).andReturn(null);
|
|
||||||
control.replay();
|
|
||||||
// Set up the connection with an old version.
|
// Set up the connection with an old version.
|
||||||
handler.connectRequested(ctx, new UpstreamChannelStateEvent(channel, ChannelState.CONNECTED, socketAddress));
|
final SettableFuture<Void> connectedFuture = SettableFuture.create();
|
||||||
VersionMessage peerVersion = new VersionMessage(unitTestParams, OTHER_PEER_CHAIN_HEIGHT);
|
final SettableFuture<Void> disconnectedFuture = SettableFuture.create();
|
||||||
peerVersion.clientVersion = 70000;
|
peer.addEventListener(new AbstractPeerEventListener() {
|
||||||
DownstreamMessageEvent versionEvent =
|
@Override
|
||||||
new DownstreamMessageEvent(channel, Channels.future(channel), peerVersion, null);
|
public void onPeerConnected(Peer peer, int peerCount) {
|
||||||
handler.messageReceived(ctx, versionEvent);
|
connectedFuture.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPeerDisconnected(Peer peer, int peerCount) {
|
||||||
|
disconnectedFuture.set(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
peer.setMinProtocolVersion(500);
|
peer.setMinProtocolVersion(500);
|
||||||
|
connectWithVersion(542);
|
||||||
|
pingAndWait(writeTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -808,7 +853,6 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
throw new RuntimeException();
|
throw new RuntimeException();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
control.replay();
|
|
||||||
connect();
|
connect();
|
||||||
Transaction t1 = new Transaction(unitTestParams);
|
Transaction t1 = new Transaction(unitTestParams);
|
||||||
t1.addInput(new TransactionInput(unitTestParams, t1, new byte[]{}));
|
t1.addInput(new TransactionInput(unitTestParams, t1, new byte[]{}));
|
||||||
@@ -816,10 +860,11 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
Transaction t2 = new Transaction(unitTestParams);
|
Transaction t2 = new Transaction(unitTestParams);
|
||||||
t2.addInput(t1.getOutput(0));
|
t2.addInput(t1.getOutput(0));
|
||||||
t2.addOutput(Utils.toNanoCoins(1, 0), wallet.getChangeAddress());
|
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 InventoryItem inventoryItem = new InventoryItem(InventoryItem.Type.Transaction, t2.getInput(0).getOutpoint().getHash());
|
||||||
final NotFoundMessage nfm = new NotFoundMessage(unitTestParams, Lists.newArrayList(inventoryItem));
|
final NotFoundMessage nfm = new NotFoundMessage(unitTestParams, Lists.newArrayList(inventoryItem));
|
||||||
inbound(peer, nfm);
|
inbound(writeTarget, nfm);
|
||||||
|
pingAndWait(writeTarget);
|
||||||
Threading.waitForUserCode();
|
Threading.waitForUserCode();
|
||||||
assertTrue(throwables[0] instanceof NullPointerException);
|
assertTrue(throwables[0] instanceof NullPointerException);
|
||||||
Threading.uncaughtExceptionHandler = null;
|
Threading.uncaughtExceptionHandler = null;
|
||||||
@@ -835,19 +880,11 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
result.setException(throwable);
|
result.setException(throwable);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ServerSocket server = new ServerSocket(0);
|
connect(); // Writes out a verack+version.
|
||||||
final NetworkParameters params = TestNet3Params.get();
|
final NetworkParameters params = TestNet3Params.testNet();
|
||||||
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.
|
|
||||||
BitcoinSerializer serializer = new BitcoinSerializer(params);
|
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.
|
// Now write some bogus truncated message.
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
serializer.serialize("inv", new InventoryMessage(params) {
|
serializer.serialize("inv", new InventoryMessage(params) {
|
||||||
@Override
|
@Override
|
||||||
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
||||||
@@ -863,22 +900,20 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
bits = Arrays.copyOf(bits, bits.length / 2);
|
bits = Arrays.copyOf(bits, bits.length / 2);
|
||||||
stream.write(bits);
|
stream.write(bits);
|
||||||
}
|
}
|
||||||
}.bitcoinSerialize(), socket.getOutputStream());
|
}.bitcoinSerialize(), out);
|
||||||
|
writeTarget.writeTarget.writeBytes(out.toByteArray());
|
||||||
try {
|
try {
|
||||||
result.get();
|
result.get();
|
||||||
fail();
|
fail();
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
assertTrue(e.getCause() instanceof ProtocolException);
|
assertTrue(e.getCause() instanceof ProtocolException);
|
||||||
}
|
}
|
||||||
}
|
try {
|
||||||
|
peer.writeTarget.writeBytes(new byte[1]);
|
||||||
// TODO: Use generics here to avoid unnecessary casting.
|
fail();
|
||||||
private Message outbound() {
|
} catch (IOException e) {
|
||||||
List<DownstreamMessageEvent> messages = event.getValues();
|
assertTrue(e instanceof ClosedChannelException ||
|
||||||
if (messages.isEmpty())
|
(e instanceof SocketException && e.getMessage().equals("Socket is closed")));
|
||||||
throw new AssertionError("No messages sent when one was expected");
|
}
|
||||||
Message message = (Message)messages.get(0).getMessage();
|
|
||||||
messages.remove(0);
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,40 +16,58 @@
|
|||||||
|
|
||||||
package com.google.bitcoin.core;
|
package com.google.bitcoin.core;
|
||||||
|
|
||||||
|
import com.google.bitcoin.networkabstraction.*;
|
||||||
import com.google.bitcoin.params.UnitTestParams;
|
import com.google.bitcoin.params.UnitTestParams;
|
||||||
import com.google.bitcoin.store.BlockStore;
|
import com.google.bitcoin.store.BlockStore;
|
||||||
import com.google.bitcoin.store.MemoryBlockStore;
|
import com.google.bitcoin.store.MemoryBlockStore;
|
||||||
import com.google.bitcoin.utils.BriefLogFormatter;
|
import com.google.bitcoin.utils.BriefLogFormatter;
|
||||||
import org.easymock.EasyMock;
|
import com.google.bitcoin.utils.Threading;
|
||||||
import org.easymock.IMocksControl;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import org.jboss.netty.channel.*;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.SocketAddress;
|
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 com.google.common.base.Preconditions.checkArgument;
|
||||||
import static org.easymock.EasyMock.expect;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class that makes it easy to work with mock NetworkConnections.
|
* Utility class that makes it easy to work with mock NetworkConnections.
|
||||||
*/
|
*/
|
||||||
public class TestWithNetworkConnections {
|
public class TestWithNetworkConnections {
|
||||||
protected IMocksControl control;
|
|
||||||
protected NetworkParameters unitTestParams;
|
protected NetworkParameters unitTestParams;
|
||||||
protected BlockStore blockStore;
|
protected BlockStore blockStore;
|
||||||
protected BlockChain blockChain;
|
protected BlockChain blockChain;
|
||||||
protected Wallet wallet;
|
protected Wallet wallet;
|
||||||
protected ECKey key;
|
protected ECKey key;
|
||||||
protected Address address;
|
protected Address address;
|
||||||
private static int fakePort;
|
|
||||||
protected ChannelHandlerContext ctx;
|
|
||||||
protected Channel channel;
|
|
||||||
protected SocketAddress socketAddress;
|
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 {
|
public void setUp() throws Exception {
|
||||||
setUp(new MemoryBlockStore(UnitTestParams.get()));
|
setUp(new MemoryBlockStore(UnitTestParams.get()));
|
||||||
}
|
}
|
||||||
@@ -57,9 +75,6 @@ public class TestWithNetworkConnections {
|
|||||||
public void setUp(BlockStore blockStore) throws Exception {
|
public void setUp(BlockStore blockStore) throws Exception {
|
||||||
BriefLogFormatter.init();
|
BriefLogFormatter.init();
|
||||||
|
|
||||||
control = createStrictControl();
|
|
||||||
control.checkOrder(false);
|
|
||||||
|
|
||||||
unitTestParams = UnitTestParams.get();
|
unitTestParams = UnitTestParams.get();
|
||||||
Wallet.SendRequest.DEFAULT_FEE_PER_KB = BigInteger.ZERO;
|
Wallet.SendRequest.DEFAULT_FEE_PER_KB = BigInteger.ZERO;
|
||||||
this.blockStore = blockStore;
|
this.blockStore = blockStore;
|
||||||
@@ -69,77 +84,106 @@ public class TestWithNetworkConnections {
|
|||||||
wallet.addKey(key);
|
wallet.addKey(key);
|
||||||
blockChain = new BlockChain(unitTestParams, wallet, blockStore);
|
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();
|
socketAddress = new InetSocketAddress("127.0.0.1", 1111);
|
||||||
channel = createChannel();
|
|
||||||
pipeline = createPipeline(channel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tearDown() throws Exception {
|
public void tearDown() throws Exception {
|
||||||
Wallet.SendRequest.DEFAULT_FEE_PER_KB = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE;
|
Wallet.SendRequest.DEFAULT_FEE_PER_KB = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE;
|
||||||
|
peerServer.stopAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ChannelPipeline createPipeline(Channel channel) {
|
protected InboundMessageQueuer connect(Peer peer, VersionMessage versionMessage) throws Exception {
|
||||||
ChannelPipeline pipeline = control.createMock(ChannelPipeline.class);
|
checkArgument(versionMessage.hasBlockChain());
|
||||||
expect(channel.getPipeline()).andStubReturn(pipeline);
|
if (clientType == ClientType.NIO_CLIENT_MANAGER || clientType == ClientType.BLOCKING_CLIENT_MANAGER)
|
||||||
return pipeline;
|
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);
|
||||||
protected Channel createChannel() {
|
else if (clientType == ClientType.BLOCKING_CLIENT)
|
||||||
Channel channel = control.createMock(Channel.class);
|
new BlockingClient(new InetSocketAddress("127.0.0.1", 2000), peer, 100, null);
|
||||||
expect(channel.getRemoteAddress()).andStubReturn(socketAddress);
|
else
|
||||||
return channel;
|
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();
|
||||||
protected ChannelHandlerContext createChannelHandlerContext() {
|
writeTarget.peer = peer;
|
||||||
ChannelHandlerContext ctx1 = control.createMock(ChannelHandlerContext.class);
|
// Complete handshake with the peer - send/receive version(ack)s, receive bloom filter
|
||||||
ctx1.sendDownstream(EasyMock.anyObject(ChannelEvent.class));
|
writeTarget.sendMessage(versionMessage);
|
||||||
EasyMock.expectLastCall().anyTimes();
|
writeTarget.sendMessage(new VersionAck());
|
||||||
ctx1.sendUpstream(EasyMock.anyObject(ChannelEvent.class));
|
assertTrue(writeTarget.nextMessageBlocking() instanceof VersionMessage);
|
||||||
EasyMock.expectLastCall().anyTimes();
|
assertTrue(writeTarget.nextMessageBlocking() instanceof VersionAck);
|
||||||
return ctx1;
|
return writeTarget;
|
||||||
}
|
|
||||||
|
|
||||||
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 void closePeer(Peer peer) throws Exception {
|
protected void closePeer(Peer peer) throws Exception {
|
||||||
peer.getHandler().channelClosed(ctx,
|
peer.close();
|
||||||
new UpstreamChannelStateEvent(channel, ChannelState.CONNECTED, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void inbound(Peer peer, Message message) throws Exception {
|
|
||||||
peer.getHandler().messageReceived(ctx,
|
|
||||||
new UpstreamMessageEvent(channel, message, socketAddress));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void inbound(FakeChannel peerChannel, Message message) {
|
protected void inbound(InboundMessageQueuer peerChannel, Message message) {
|
||||||
Channels.fireMessageReceived(peerChannel, message);
|
peerChannel.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Object outbound(FakeChannel p1) {
|
private void outboundPingAndWait(final InboundMessageQueuer p, long nonce) throws Exception {
|
||||||
ChannelEvent channelEvent = p1.nextEvent();
|
// Send a ping and wait for it to get to the other side
|
||||||
if (channelEvent != null && !(channelEvent instanceof MessageEvent))
|
SettableFuture<Void> pingReceivedFuture = SettableFuture.create();
|
||||||
throw new IllegalStateException("Expected message but got: " + channelEvent);
|
p.mapPingFutures.put(nonce, pingReceivedFuture);
|
||||||
MessageEvent nextEvent = (MessageEvent) channelEvent;
|
p.peer.sendMessage(new Ping(nonce));
|
||||||
if (nextEvent == null)
|
pingReceivedFuture.get();
|
||||||
return null;
|
p.mapPingFutures.remove(nonce);
|
||||||
return nextEvent.getMessage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Object waitForOutbound(FakeChannel ch) throws InterruptedException {
|
private void inboundPongAndWait(final InboundMessageQueuer p, final long nonce) throws Exception {
|
||||||
return ((MessageEvent)ch.nextEventBlocking()).getMessage();
|
// 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) {
|
protected void pingAndWait(final InboundMessageQueuer p) throws Exception {
|
||||||
return PeerGroup.peerFromChannel(ch);
|
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;
|
package com.google.bitcoin.core;
|
||||||
|
|
||||||
import com.google.bitcoin.params.UnitTestParams;
|
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 com.google.bitcoin.store.BlockStore;
|
||||||
import org.jboss.netty.bootstrap.ClientBootstrap;
|
|
||||||
import org.jboss.netty.channel.*;
|
|
||||||
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,55 +34,58 @@ public class TestWithPeerGroup extends TestWithNetworkConnections {
|
|||||||
protected PeerGroup peerGroup;
|
protected PeerGroup peerGroup;
|
||||||
|
|
||||||
protected VersionMessage remoteVersionMessage;
|
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 {
|
public void setUp(BlockStore blockStore) throws Exception {
|
||||||
super.setUp(blockStore);
|
super.setUp(blockStore);
|
||||||
|
|
||||||
remoteVersionMessage = new VersionMessage(unitTestParams, 1);
|
remoteVersionMessage = new VersionMessage(unitTestParams, 1);
|
||||||
|
remoteVersionMessage.localServices = VersionMessage.NODE_NETWORK;
|
||||||
remoteVersionMessage.clientVersion = FilteredBlock.MIN_PROTOCOL_VERSION;
|
remoteVersionMessage.clientVersion = FilteredBlock.MIN_PROTOCOL_VERSION;
|
||||||
initPeerGroup();
|
initPeerGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void initPeerGroup() {
|
protected void initPeerGroup() {
|
||||||
bootstrap = new ClientBootstrap(new ChannelFactory() {
|
if (clientType == ClientType.NIO_CLIENT_MANAGER)
|
||||||
public void releaseExternalResources() {}
|
peerGroup = new PeerGroup(unitTestParams, blockChain, new NioClientManager());
|
||||||
public Channel newChannel(ChannelPipeline pipeline) {
|
else
|
||||||
ChannelSink sink = new FakeChannelSink();
|
peerGroup = new PeerGroup(unitTestParams, blockChain, new BlockingClientManager());
|
||||||
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);
|
|
||||||
peerGroup.setPingIntervalMsec(0); // Disable the pings as they just get in the way of most tests.
|
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);
|
return connectPeer(id, remoteVersionMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected FakeChannel connectPeer(int id, VersionMessage versionMessage) {
|
protected InboundMessageQueuer connectPeer(int id, VersionMessage versionMessage) throws Exception {
|
||||||
InetSocketAddress remoteAddress = new InetSocketAddress("127.0.0.1", 2000 + id);
|
checkArgument(versionMessage.hasBlockChain());
|
||||||
FakeChannel p = (FakeChannel) peerGroup.connectTo(remoteAddress).getChannel();
|
InboundMessageQueuer writeTarget = connectPeerWithoutVersionExchange(id);
|
||||||
assertTrue(p.nextEvent() instanceof ChannelStateEvent);
|
// Complete handshake with the peer - send/receive version(ack)s, receive bloom filter
|
||||||
inbound(p, versionMessage);
|
writeTarget.sendMessage(versionMessage);
|
||||||
inbound(p, new VersionAck());
|
writeTarget.sendMessage(new VersionAck());
|
||||||
|
assertTrue(writeTarget.nextMessageBlocking() instanceof VersionMessage);
|
||||||
|
assertTrue(writeTarget.nextMessageBlocking() instanceof VersionAck);
|
||||||
if (versionMessage.isBloomFilteringSupported()) {
|
if (versionMessage.isBloomFilteringSupported()) {
|
||||||
assertTrue(outbound(p) instanceof BloomFilter);
|
assertTrue(writeTarget.nextMessageBlocking() instanceof BloomFilter);
|
||||||
assertTrue(outbound(p) instanceof MemoryPoolMessage);
|
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.TestUtils;
|
||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
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 java.util.Random;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(value = Parameterized.class)
|
||||||
public class TransactionBroadcastTest extends TestWithPeerGroup {
|
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
|
@Override
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
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.
|
// Fix the random permutation that TransactionBroadcast uses to shuffle the peers.
|
||||||
TransactionBroadcast.random = new Random(0);
|
TransactionBroadcast.random = new Random(0);
|
||||||
peerGroup.setMinBroadcastConnections(2);
|
peerGroup.setMinBroadcastConnections(2);
|
||||||
|
peerGroup.startAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() throws Exception {
|
||||||
|
super.tearDown();
|
||||||
|
peerGroup.stopAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void fourPeers() throws Exception {
|
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);
|
Transaction tx = new Transaction(params);
|
||||||
TransactionBroadcast broadcast = new TransactionBroadcast(peerGroup, tx);
|
TransactionBroadcast broadcast = new TransactionBroadcast(peerGroup, tx);
|
||||||
ListenableFuture<Transaction> future = broadcast.broadcast();
|
ListenableFuture<Transaction> future = broadcast.broadcast();
|
||||||
@@ -64,6 +89,7 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
|
|||||||
Threading.waitForUserCode();
|
Threading.waitForUserCode();
|
||||||
assertFalse(future.isDone());
|
assertFalse(future.isDone());
|
||||||
inbound(channels[1], InventoryMessage.with(tx));
|
inbound(channels[1], InventoryMessage.with(tx));
|
||||||
|
pingAndWait(channels[1]);
|
||||||
Threading.waitForUserCode();
|
Threading.waitForUserCode();
|
||||||
assertTrue(future.isDone());
|
assertTrue(future.isDone());
|
||||||
}
|
}
|
||||||
@@ -72,7 +98,7 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
|
|||||||
public void retryFailedBroadcast() throws Exception {
|
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
|
// 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.
|
// the tx should be broadcast again.
|
||||||
FakeChannel p1 = connectPeer(1, new VersionMessage(params, 2));
|
InboundMessageQueuer p1 = connectPeer(1);
|
||||||
connectPeer(2);
|
connectPeer(2);
|
||||||
|
|
||||||
// Send ourselves a bit of money.
|
// 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.
|
// p1 eats it :( A bit later the PeerGroup is taken down.
|
||||||
peerGroup.removeWallet(wallet);
|
peerGroup.removeWallet(wallet);
|
||||||
// ... and put back.
|
|
||||||
initPeerGroup();
|
|
||||||
peerGroup.addWallet(wallet);
|
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
|
// 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
|
// 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.
|
// Make sure we can create spends, and that they are announced. Then do the same with offline mode.
|
||||||
|
|
||||||
// Set up connections and block chain.
|
// Set up connections and block chain.
|
||||||
FakeChannel p1 = connectPeer(1, new VersionMessage(params, 2));
|
VersionMessage ver = new VersionMessage(params, 2);
|
||||||
FakeChannel p2 = connectPeer(2);
|
ver.localServices = VersionMessage.NODE_NETWORK;
|
||||||
|
InboundMessageQueuer p1 = connectPeer(1, ver);
|
||||||
|
InboundMessageQueuer p2 = connectPeer(2);
|
||||||
|
|
||||||
// Send ourselves a bit of money.
|
// Send ourselves a bit of money.
|
||||||
Block b1 = TestUtils.makeSolvedTestBlock(blockStore, address);
|
Block b1 = TestUtils.makeSolvedTestBlock(blockStore, address);
|
||||||
inbound(p1, b1);
|
inbound(p1, b1);
|
||||||
|
pingAndWait(p1);
|
||||||
assertNull(outbound(p1));
|
assertNull(outbound(p1));
|
||||||
assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());
|
assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());
|
||||||
|
|
||||||
@@ -143,6 +168,7 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
|
|||||||
InventoryMessage inv = new InventoryMessage(params);
|
InventoryMessage inv = new InventoryMessage(params);
|
||||||
inv.addTransaction(t1);
|
inv.addTransaction(t1);
|
||||||
inbound(p2, inv);
|
inbound(p2, inv);
|
||||||
|
pingAndWait(p2);
|
||||||
Threading.waitForUserCode();
|
Threading.waitForUserCode();
|
||||||
assertTrue(sendResult.broadcastComplete.isDone());
|
assertTrue(sendResult.broadcastComplete.isDone());
|
||||||
assertEquals(transactions[0], sendResult.tx);
|
assertEquals(transactions[0], sendResult.tx);
|
||||||
@@ -150,6 +176,7 @@ public class TransactionBroadcastTest extends TestWithPeerGroup {
|
|||||||
// Confirm it.
|
// Confirm it.
|
||||||
Block b2 = TestUtils.createFakeBlock(blockStore, t1).block;
|
Block b2 = TestUtils.createFakeBlock(blockStore, t1).block;
|
||||||
inbound(p1, b2);
|
inbound(p1, b2);
|
||||||
|
pingAndWait(p1);
|
||||||
assertNull(outbound(p1));
|
assertNull(outbound(p1));
|
||||||
|
|
||||||
// Do the same thing with an offline transaction.
|
// Do the same thing with an offline transaction.
|
||||||
|
|||||||
@@ -14,11 +14,14 @@
|
|||||||
* limitations under the License.
|
* 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.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketAddress;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import com.google.bitcoin.core.Utils;
|
import com.google.bitcoin.core.Utils;
|
||||||
@@ -28,12 +31,48 @@ import org.bitcoin.paymentchannel.Protos;
|
|||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkState;
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
public class NioWrapperTest {
|
@RunWith(value = Parameterized.class)
|
||||||
|
public class NetworkAbstractionTests {
|
||||||
private AtomicBoolean fail;
|
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
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
@@ -76,8 +115,8 @@ public class NioWrapperTest {
|
|||||||
}
|
}
|
||||||
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
|
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
|
||||||
}
|
}
|
||||||
});
|
}, new InetSocketAddress("localhost", 4243));
|
||||||
server.start(new InetSocketAddress("localhost", 4243));
|
server.startAndWait();
|
||||||
|
|
||||||
ProtobufParser<Protos.TwoWayChannelMessage> clientHandler = new ProtobufParser<Protos.TwoWayChannelMessage>(
|
ProtobufParser<Protos.TwoWayChannelMessage> clientHandler = new ProtobufParser<Protos.TwoWayChannelMessage>(
|
||||||
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
|
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
|
||||||
@@ -100,7 +139,7 @@ public class NioWrapperTest {
|
|||||||
}
|
}
|
||||||
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
|
}, 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();
|
clientConnectionOpen.get();
|
||||||
serverConnectionOpen.get();
|
serverConnectionOpen.get();
|
||||||
@@ -114,7 +153,8 @@ public class NioWrapperTest {
|
|||||||
serverConnectionClosed.get();
|
serverConnectionClosed.get();
|
||||||
clientConnectionClosed.get();
|
clientConnectionClosed.get();
|
||||||
|
|
||||||
server.stop();
|
server.stopAndWait();
|
||||||
|
assertFalse(server.isRunning());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -157,10 +197,10 @@ public class NioWrapperTest {
|
|||||||
}
|
}
|
||||||
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 10);
|
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 10);
|
||||||
}
|
}
|
||||||
});
|
}, new InetSocketAddress("localhost", 4243));
|
||||||
server.start(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>() {
|
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
|
||||||
@Override
|
@Override
|
||||||
public void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) {
|
public void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) {
|
||||||
@@ -176,7 +216,7 @@ public class NioWrapperTest {
|
|||||||
public void connectionClosed(ProtobufParser handler) {
|
public void connectionClosed(ProtobufParser handler) {
|
||||||
clientConnection1Closed.set(null);
|
clientConnection1Closed.set(null);
|
||||||
}
|
}
|
||||||
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0), 0);
|
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0));
|
||||||
|
|
||||||
clientConnection1Open.get();
|
clientConnection1Open.get();
|
||||||
serverConnection1Open.get();
|
serverConnection1Open.get();
|
||||||
@@ -202,7 +242,7 @@ public class NioWrapperTest {
|
|||||||
clientConnection2Closed.set(null);
|
clientConnection2Closed.set(null);
|
||||||
}
|
}
|
||||||
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
|
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
|
||||||
NioClient client2 = new NioClient(new InetSocketAddress("localhost", 4243), client2Handler, 0);
|
openConnection(new InetSocketAddress("localhost", 4243), client2Handler);
|
||||||
|
|
||||||
clientConnection2Open.get();
|
clientConnection2Open.get();
|
||||||
serverConnection2Open.get();
|
serverConnection2Open.get();
|
||||||
@@ -213,7 +253,7 @@ public class NioWrapperTest {
|
|||||||
clientConnection2Closed.get();
|
clientConnection2Closed.get();
|
||||||
serverConnection2Closed.get();
|
serverConnection2Closed.get();
|
||||||
|
|
||||||
server.stop();
|
server.stopAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -247,8 +287,8 @@ public class NioWrapperTest {
|
|||||||
}
|
}
|
||||||
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 0x10000, 0);
|
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 0x10000, 0);
|
||||||
}
|
}
|
||||||
});
|
}, new InetSocketAddress("localhost", 4243));
|
||||||
server.start(new InetSocketAddress("localhost", 4243));
|
server.startAndWait();
|
||||||
|
|
||||||
ProtobufParser<Protos.TwoWayChannelMessage> clientHandler = new ProtobufParser<Protos.TwoWayChannelMessage>(
|
ProtobufParser<Protos.TwoWayChannelMessage> clientHandler = new ProtobufParser<Protos.TwoWayChannelMessage>(
|
||||||
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
|
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
|
||||||
@@ -279,7 +319,7 @@ public class NioWrapperTest {
|
|||||||
}
|
}
|
||||||
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 0x10000, 0);
|
}, 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();
|
clientConnectionOpen.get();
|
||||||
serverConnectionOpen.get();
|
serverConnectionOpen.get();
|
||||||
@@ -358,7 +398,7 @@ public class NioWrapperTest {
|
|||||||
serverConnectionClosed.get();
|
serverConnectionClosed.get();
|
||||||
clientConnectionClosed.get();
|
clientConnectionClosed.get();
|
||||||
|
|
||||||
server.stop();
|
server.stopAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -411,8 +451,8 @@ public class NioWrapperTest {
|
|||||||
}
|
}
|
||||||
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
|
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
|
||||||
}
|
}
|
||||||
});
|
}, new InetSocketAddress("localhost", 4243));
|
||||||
server.start(new InetSocketAddress("localhost", 4243));
|
server.startAndWait();
|
||||||
|
|
||||||
ProtobufParser<Protos.TwoWayChannelMessage> client1Handler = new ProtobufParser<Protos.TwoWayChannelMessage>(
|
ProtobufParser<Protos.TwoWayChannelMessage> client1Handler = new ProtobufParser<Protos.TwoWayChannelMessage>(
|
||||||
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
|
new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() {
|
||||||
@@ -431,7 +471,7 @@ public class NioWrapperTest {
|
|||||||
client1ConnectionClosed.set(null);
|
client1ConnectionClosed.set(null);
|
||||||
}
|
}
|
||||||
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
|
}, 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();
|
client1ConnectionOpen.get();
|
||||||
serverConnection1Open.get();
|
serverConnection1Open.get();
|
||||||
@@ -453,7 +493,7 @@ public class NioWrapperTest {
|
|||||||
client2ConnectionClosed.set(null);
|
client2ConnectionClosed.set(null);
|
||||||
}
|
}
|
||||||
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
|
}, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0);
|
||||||
NioClient client2 = new NioClient(new InetSocketAddress("localhost", 4243), client2Handler, 0);
|
openConnection(new InetSocketAddress("localhost", 4243), client2Handler);
|
||||||
|
|
||||||
client2ConnectionOpen.get();
|
client2ConnectionOpen.get();
|
||||||
serverConnection2Open.get();
|
serverConnection2Open.get();
|
||||||
@@ -497,17 +537,18 @@ public class NioWrapperTest {
|
|||||||
client3Handler.write(msg3);
|
client3Handler.write(msg3);
|
||||||
assertEquals(msg3, client3MessageReceived.get());
|
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
|
// This often triggers ClosedByInterruptException in handleKey
|
||||||
server.handlerThread.interrupt();
|
server.stop();
|
||||||
|
server.selector.wakeup();
|
||||||
client3.closeConnection();
|
client3.closeConnection();
|
||||||
client3ConnectionClosed.get();
|
client3ConnectionClosed.get();
|
||||||
serverConnectionClosed3.get();
|
serverConnectionClosed3.get();
|
||||||
|
|
||||||
server.handlerThread.join();
|
server.stopAndWait();
|
||||||
client2ConnectionClosed.get();
|
client2ConnectionClosed.get();
|
||||||
serverConnectionClosed2.get();
|
serverConnectionClosed2.get();
|
||||||
|
|
||||||
server.stop();
|
server.stopAndWait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,17 +16,21 @@
|
|||||||
|
|
||||||
package com.google.bitcoin.examples;
|
package com.google.bitcoin.examples;
|
||||||
|
|
||||||
|
import com.google.bitcoin.core.AbstractPeerEventListener;
|
||||||
import com.google.bitcoin.core.NetworkParameters;
|
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.core.VersionMessage;
|
||||||
import com.google.bitcoin.discovery.DnsDiscovery;
|
import com.google.bitcoin.discovery.DnsDiscovery;
|
||||||
import com.google.bitcoin.discovery.PeerDiscoveryException;
|
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.params.MainNetParams;
|
||||||
import com.google.bitcoin.utils.BriefLogFormatter;
|
import com.google.bitcoin.utils.BriefLogFormatter;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
@@ -74,16 +78,16 @@ public class PrintPeers {
|
|||||||
final Object lock = new Object();
|
final Object lock = new Object();
|
||||||
final long[] bestHeight = new long[1];
|
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) {
|
for (final InetAddress addr : addrs) {
|
||||||
final ListenableFuture<TCPNetworkConnection> future =
|
final Peer peer = new Peer(params, new VersionMessage(params, 0), null, new InetSocketAddress(addr, params.getPort()));
|
||||||
TCPNetworkConnection.connectTo(params, new InetSocketAddress(addr, params.getPort()), 1000 /* timeout */, null);
|
final SettableFuture future = SettableFuture.create();
|
||||||
futures.add(future);
|
|
||||||
// Once the connection has completed version handshaking ...
|
// Once the connection has completed version handshaking ...
|
||||||
Futures.addCallback(future, new FutureCallback<TCPNetworkConnection>() {
|
peer.addEventListener(new AbstractPeerEventListener() {
|
||||||
public void onSuccess(TCPNetworkConnection conn) {
|
public void onPeerConnected(Peer p, int peerCount) {
|
||||||
// Check the chain height it claims to have.
|
// Check the chain height it claims to have.
|
||||||
VersionMessage ver = conn.getVersionMessage();
|
VersionMessage ver = peer.getPeerVersionMessage();
|
||||||
long nodeHeight = ver.bestHeight;
|
long nodeHeight = ver.bestHeight;
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
long diff = bestHeight[0] - nodeHeight;
|
long diff = bestHeight[0] - nodeHeight;
|
||||||
@@ -97,13 +101,19 @@ public class PrintPeers {
|
|||||||
bestHeight[0] = nodeHeight;
|
bestHeight[0] = nodeHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conn.close();
|
// Now finish the future and close the connection
|
||||||
|
future.set(null);
|
||||||
|
peer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onFailure(Throwable throwable) {
|
public void onPeerDisconnected(Peer p, int peerCount) {
|
||||||
System.out.println("Failed to talk to " + addr + ": " + throwable.getMessage());
|
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.
|
// Wait for every tried connection to finish.
|
||||||
Futures.successfulAsList(futures).get();
|
Futures.successfulAsList(futures).get();
|
||||||
|
|||||||
7
pom.xml
7
pom.xml
@@ -102,13 +102,6 @@
|
|||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.netty</groupId>
|
|
||||||
<artifactId>netty</artifactId>
|
|
||||||
<version>3.6.3.Final</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.madgag</groupId>
|
<groupId>com.madgag</groupId>
|
||||||
<artifactId>sc-light-jdk15on</artifactId>
|
<artifactId>sc-light-jdk15on</artifactId>
|
||||||
|
|||||||
Reference in New Issue
Block a user