/** * Closes the channel with the client (will generate a {@link * ServerConnectionEventHandler#channelClosed(PaymentChannelCloseException.CloseReason)} event) * * <p>Note that this does <i>NOT</i> actually broadcast the most recent payment transaction, which * will be triggered automatically when the channel times out by the {@link * StoredPaymentChannelServerStates}, or manually by calling {@link * StoredPaymentChannelServerStates#closeChannel(StoredServerChannel)} with the channel returned * by {@link StoredPaymentChannelServerStates#getChannel(com.google.bitcoin.core.Sha256Hash)} with * the id provided in {@link * ServerConnectionEventHandler#channelOpen(com.google.bitcoin.core.Sha256Hash)} */ @SuppressWarnings("unchecked") // The warning 'unchecked call to write(MessageType)' being suppressed here comes from the build() // formally returning MessageLite-derived class that cannot be statically guaranteed to be the // same MessageType // that is used in connectionChannel. protected final synchronized void closeChannel() { if (connectionChannel == null) throw new IllegalStateException("Channel is not fully initialized/has already been closed"); connectionChannel.write( Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.CLOSE) .build()); connectionChannel.closeConnection(); }
@Test public void basicClientServerTest() throws Exception { // Tests creating a basic server, opening a client connection and sending a few messages final SettableFuture<Void> serverConnectionOpen = SettableFuture.create(); final SettableFuture<Void> clientConnectionOpen = SettableFuture.create(); final SettableFuture<Void> serverConnectionClosed = SettableFuture.create(); final SettableFuture<Void> clientConnectionClosed = SettableFuture.create(); final SettableFuture<Protos.TwoWayChannelMessage> clientMessage1Received = SettableFuture.create(); final SettableFuture<Protos.TwoWayChannelMessage> clientMessage2Received = SettableFuture.create(); NioServer server = new NioServer( new StreamParserFactory() { @Override public ProtobufParser getNewParser(InetAddress inetAddress, int port) { return new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public void messageReceived( ProtobufParser<Protos.TwoWayChannelMessage> handler, Protos.TwoWayChannelMessage msg) { handler.write(msg); handler.write(msg); } @Override public void connectionOpen(ProtobufParser handler) { serverConnectionOpen.set(null); } @Override public void connectionClosed(ProtobufParser handler) { serverConnectionClosed.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0); } }, new InetSocketAddress("localhost", 4243)); server.startAndWait(); ProtobufParser<Protos.TwoWayChannelMessage> clientHandler = new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public synchronized void messageReceived( ProtobufParser handler, Protos.TwoWayChannelMessage msg) { if (clientMessage1Received.isDone()) clientMessage2Received.set(msg); else clientMessage1Received.set(msg); } @Override public void connectionOpen(ProtobufParser handler) { clientConnectionOpen.set(null); } @Override public void connectionClosed(ProtobufParser handler) { clientConnectionClosed.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0); MessageWriteTarget client = openConnection(new InetSocketAddress("localhost", 4243), clientHandler); clientConnectionOpen.get(); serverConnectionOpen.get(); Protos.TwoWayChannelMessage msg = Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN) .build(); clientHandler.write(msg); assertEquals(msg, clientMessage1Received.get()); assertEquals(msg, clientMessage2Received.get()); client.closeConnection(); serverConnectionClosed.get(); clientConnectionClosed.get(); server.stopAndWait(); assertFalse(server.isRunning()); }
@Test public void testConnectionEventHandlers() throws Exception { final SettableFuture<Void> serverConnection1Open = SettableFuture.create(); final SettableFuture<Void> serverConnection2Open = SettableFuture.create(); final SettableFuture<Void> serverConnection3Open = SettableFuture.create(); final SettableFuture<Void> client1ConnectionOpen = SettableFuture.create(); final SettableFuture<Void> client2ConnectionOpen = SettableFuture.create(); final SettableFuture<Void> client3ConnectionOpen = SettableFuture.create(); final SettableFuture<Void> serverConnectionClosed1 = SettableFuture.create(); final SettableFuture<Void> serverConnectionClosed2 = SettableFuture.create(); final SettableFuture<Void> serverConnectionClosed3 = SettableFuture.create(); final SettableFuture<Void> client1ConnectionClosed = SettableFuture.create(); final SettableFuture<Void> client2ConnectionClosed = SettableFuture.create(); final SettableFuture<Void> client3ConnectionClosed = SettableFuture.create(); final SettableFuture<Protos.TwoWayChannelMessage> client1MessageReceived = SettableFuture.create(); final SettableFuture<Protos.TwoWayChannelMessage> client2MessageReceived = SettableFuture.create(); final SettableFuture<Protos.TwoWayChannelMessage> client3MessageReceived = SettableFuture.create(); NioServer server = new NioServer( new StreamParserFactory() { @Override public ProtobufParser getNewParser(InetAddress inetAddress, int port) { return new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public void messageReceived( ProtobufParser<Protos.TwoWayChannelMessage> handler, Protos.TwoWayChannelMessage msg) { handler.write(msg); } @Override public synchronized void connectionOpen(ProtobufParser handler) { if (serverConnection1Open.isDone()) { if (serverConnection2Open.isDone()) serverConnection3Open.set(null); else serverConnection2Open.set(null); } else serverConnection1Open.set(null); } @Override public synchronized void connectionClosed(ProtobufParser handler) { if (serverConnectionClosed1.isDone()) { if (serverConnectionClosed2.isDone()) { checkState(!serverConnectionClosed3.isDone()); serverConnectionClosed3.set(null); } else serverConnectionClosed2.set(null); } else serverConnectionClosed1.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0); } }, new InetSocketAddress("localhost", 4243)); server.startAndWait(); ProtobufParser<Protos.TwoWayChannelMessage> client1Handler = new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) { client1MessageReceived.set(msg); } @Override public void connectionOpen(ProtobufParser handler) { client1ConnectionOpen.set(null); } @Override public void connectionClosed(ProtobufParser handler) { client1ConnectionClosed.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0); MessageWriteTarget client1 = openConnection(new InetSocketAddress("localhost", 4243), client1Handler); client1ConnectionOpen.get(); serverConnection1Open.get(); ProtobufParser<Protos.TwoWayChannelMessage> client2Handler = new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) { client2MessageReceived.set(msg); } @Override public void connectionOpen(ProtobufParser handler) { client2ConnectionOpen.set(null); } @Override public void connectionClosed(ProtobufParser handler) { client2ConnectionClosed.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0); openConnection(new InetSocketAddress("localhost", 4243), client2Handler); client2ConnectionOpen.get(); serverConnection2Open.get(); ProtobufParser<Protos.TwoWayChannelMessage> client3Handler = new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) { client3MessageReceived.set(msg); } @Override public void connectionOpen(ProtobufParser handler) { client3ConnectionOpen.set(null); } @Override public synchronized void connectionClosed(ProtobufParser handler) { checkState(!client3ConnectionClosed.isDone()); client3ConnectionClosed.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0); NioClient client3 = new NioClient(new InetSocketAddress("localhost", 4243), client3Handler, 0); client3ConnectionOpen.get(); serverConnection3Open.get(); Protos.TwoWayChannelMessage msg = Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN) .build(); client1Handler.write(msg); assertEquals(msg, client1MessageReceived.get()); Protos.TwoWayChannelMessage msg2 = Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.INITIATE) .build(); client2Handler.write(msg2); assertEquals(msg2, client2MessageReceived.get()); client1.closeConnection(); serverConnectionClosed1.get(); client1ConnectionClosed.get(); Protos.TwoWayChannelMessage msg3 = Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.CLOSE) .build(); client3Handler.write(msg3); assertEquals(msg3, client3MessageReceived.get()); // Try to create a race condition by triggering handlerThread closing and client3 closing at the // same time // This often triggers ClosedByInterruptException in handleKey server.stop(); server.selector.wakeup(); client3.closeConnection(); client3ConnectionClosed.get(); serverConnectionClosed3.get(); server.stopAndWait(); client2ConnectionClosed.get(); serverConnectionClosed2.get(); server.stopAndWait(); }
@Test public void largeDataTest() throws Exception { /** * Test various large-data handling, essentially testing {@link * ProtobufParser#receiveBytes(java.nio.ByteBuffer)} */ final SettableFuture<Void> serverConnectionOpen = SettableFuture.create(); final SettableFuture<Void> clientConnectionOpen = SettableFuture.create(); final SettableFuture<Void> serverConnectionClosed = SettableFuture.create(); final SettableFuture<Void> clientConnectionClosed = SettableFuture.create(); final SettableFuture<Protos.TwoWayChannelMessage> clientMessage1Received = SettableFuture.create(); final SettableFuture<Protos.TwoWayChannelMessage> clientMessage2Received = SettableFuture.create(); final SettableFuture<Protos.TwoWayChannelMessage> clientMessage3Received = SettableFuture.create(); final SettableFuture<Protos.TwoWayChannelMessage> clientMessage4Received = SettableFuture.create(); NioServer server = new NioServer( new StreamParserFactory() { @Override public ProtobufParser getNewParser(InetAddress inetAddress, int port) { return new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public void messageReceived( ProtobufParser<Protos.TwoWayChannelMessage> handler, Protos.TwoWayChannelMessage msg) { handler.write(msg); } @Override public void connectionOpen(ProtobufParser handler) { serverConnectionOpen.set(null); } @Override public void connectionClosed(ProtobufParser handler) { serverConnectionClosed.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 0x10000, 0); } }, new InetSocketAddress("localhost", 4243)); server.startAndWait(); ProtobufParser<Protos.TwoWayChannelMessage> clientHandler = new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public synchronized void messageReceived( ProtobufParser handler, Protos.TwoWayChannelMessage msg) { if (clientMessage1Received.isDone()) { if (clientMessage2Received.isDone()) { if (clientMessage3Received.isDone()) { if (clientMessage4Received.isDone()) fail.set(true); clientMessage4Received.set(msg); } else clientMessage3Received.set(msg); } else clientMessage2Received.set(msg); } else clientMessage1Received.set(msg); } @Override public void connectionOpen(ProtobufParser handler) { clientConnectionOpen.set(null); } @Override public void connectionClosed(ProtobufParser handler) { clientConnectionClosed.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 0x10000, 0); MessageWriteTarget client = openConnection(new InetSocketAddress("localhost", 4243), clientHandler); clientConnectionOpen.get(); serverConnectionOpen.get(); // Large message that is larger than buffer and equal to maximum message size Protos.TwoWayChannelMessage msg = Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN) .setClientVersion( Protos.ClientVersion.newBuilder() .setMajor(1) .setPreviousChannelContractHash(ByteString.copyFrom(new byte[0x10000 - 12]))) .build(); // Small message that fits in the buffer Protos.TwoWayChannelMessage msg2 = Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN) .setClientVersion( Protos.ClientVersion.newBuilder() .setMajor(1) .setPreviousChannelContractHash(ByteString.copyFrom(new byte[1]))) .build(); // Break up the message into chunks to simulate packet network (with strange MTUs...) byte[] messageBytes = msg.toByteArray(); byte[] messageLength = new byte[4]; Utils.uint32ToByteArrayBE(messageBytes.length, messageLength, 0); client.writeBytes(new byte[] {messageLength[0], messageLength[1]}); Thread.sleep(10); client.writeBytes(new byte[] {messageLength[2], messageLength[3]}); Thread.sleep(10); client.writeBytes(new byte[] {messageBytes[0], messageBytes[1]}); Thread.sleep(10); client.writeBytes(Arrays.copyOfRange(messageBytes, 2, messageBytes.length - 1)); Thread.sleep(10); // Now send the end of msg + msg2 + msg3 all at once byte[] messageBytes2 = msg2.toByteArray(); byte[] messageLength2 = new byte[4]; Utils.uint32ToByteArrayBE(messageBytes2.length, messageLength2, 0); byte[] sendBytes = Arrays.copyOf( new byte[] {messageBytes[messageBytes.length - 1]}, 1 + messageBytes2.length * 2 + messageLength2.length * 2); System.arraycopy(messageLength2, 0, sendBytes, 1, 4); System.arraycopy(messageBytes2, 0, sendBytes, 5, messageBytes2.length); System.arraycopy(messageLength2, 0, sendBytes, 5 + messageBytes2.length, 4); System.arraycopy(messageBytes2, 0, sendBytes, 9 + messageBytes2.length, messageBytes2.length); client.writeBytes(sendBytes); assertEquals(msg, clientMessage1Received.get()); assertEquals(msg2, clientMessage2Received.get()); assertEquals(msg2, clientMessage3Received.get()); // Now resent msg2 in chunks, by itself Utils.uint32ToByteArrayBE(messageBytes2.length, messageLength2, 0); client.writeBytes(new byte[] {messageLength2[0], messageLength2[1]}); Thread.sleep(10); client.writeBytes(new byte[] {messageLength2[2], messageLength2[3]}); Thread.sleep(10); client.writeBytes(new byte[] {messageBytes2[0], messageBytes2[1]}); Thread.sleep(10); client.writeBytes(new byte[] {messageBytes2[2], messageBytes2[3]}); Thread.sleep(10); client.writeBytes(Arrays.copyOfRange(messageBytes2, 4, messageBytes2.length)); assertEquals(msg2, clientMessage4Received.get()); Protos.TwoWayChannelMessage msg5 = Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN) .setClientVersion( Protos.ClientVersion.newBuilder() .setMajor(1) .setPreviousChannelContractHash(ByteString.copyFrom(new byte[0x10000 - 11]))) .build(); try { clientHandler.write(msg5); } catch (IllegalStateException e) { } // Override max size and make sure the server drops our connection byte[] messageLength5 = new byte[4]; Utils.uint32ToByteArrayBE(msg5.toByteArray().length, messageLength5, 0); client.writeBytes(messageLength5); serverConnectionClosed.get(); clientConnectionClosed.get(); server.stopAndWait(); }
@Test public void basicTimeoutTest() throws Exception { // Tests various timeout scenarios final SettableFuture<Void> serverConnection1Open = SettableFuture.create(); final SettableFuture<Void> clientConnection1Open = SettableFuture.create(); final SettableFuture<Void> serverConnection1Closed = SettableFuture.create(); final SettableFuture<Void> clientConnection1Closed = SettableFuture.create(); final SettableFuture<Void> serverConnection2Open = SettableFuture.create(); final SettableFuture<Void> clientConnection2Open = SettableFuture.create(); final SettableFuture<Void> serverConnection2Closed = SettableFuture.create(); final SettableFuture<Void> clientConnection2Closed = SettableFuture.create(); NioServer server = new NioServer( new StreamParserFactory() { @Override public ProtobufParser getNewParser(InetAddress inetAddress, int port) { return new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public void messageReceived( ProtobufParser handler, Protos.TwoWayChannelMessage msg) { fail.set(true); } @Override public synchronized void connectionOpen(ProtobufParser handler) { if (serverConnection1Open.isDone()) { handler.setSocketTimeout(0); serverConnection2Open.set(null); } else serverConnection1Open.set(null); } @Override public synchronized void connectionClosed(ProtobufParser handler) { if (serverConnection1Closed.isDone()) { serverConnection2Closed.set(null); } else serverConnection1Closed.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 10); } }, new InetSocketAddress("localhost", 4243)); server.startAndWait(); openConnection( new InetSocketAddress("localhost", 4243), new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) { fail.set(true); } @Override public void connectionOpen(ProtobufParser handler) { clientConnection1Open.set(null); } @Override public void connectionClosed(ProtobufParser handler) { clientConnection1Closed.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0)); clientConnection1Open.get(); serverConnection1Open.get(); long closeDelayStart = System.currentTimeMillis(); clientConnection1Closed.get(); serverConnection1Closed.get(); long closeDelayFinish = System.currentTimeMillis(); ProtobufParser<Protos.TwoWayChannelMessage> client2Handler = new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) { fail.set(true); } @Override public void connectionOpen(ProtobufParser handler) { clientConnection2Open.set(null); } @Override public void connectionClosed(ProtobufParser handler) { clientConnection2Closed.set(null); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), 1000, 0); openConnection(new InetSocketAddress("localhost", 4243), client2Handler); clientConnection2Open.get(); serverConnection2Open.get(); Thread.sleep((closeDelayFinish - closeDelayStart) * 10); assertFalse(clientConnection2Closed.isDone() || serverConnection2Closed.isDone()); client2Handler.setSocketTimeout(10); clientConnection2Closed.get(); serverConnection2Closed.get(); server.stopAndWait(); }
/** * Attempts to open a new connection to and open a payment channel with the given host and port, * blocking until the connection is open * * @param server The host/port pair where the server is listening. * @param timeoutSeconds The connection timeout and read timeout during initialization. This * should be large enough to accommodate ECDSA signature operations and network latency. * @param wallet The wallet which will be paid from, and where completed transactions will be * committed. Must already have a {@link StoredPaymentChannelClientStates} object in its * extensions set. * @param myKey A freshly generated keypair used for the multisig contract and refund output. * @param maxValue The maximum value this channel is allowed to request * @param serverId A unique ID which is used to attempt reopening of an existing channel. This * must be unique to the server, and, if your application is exposing payment channels to some * API, this should also probably encompass some caller UID to avoid applications opening * channels which were created by others. * @throws IOException if there's an issue using the network. * @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue. */ public PaymentChannelClientConnection( InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey, BigInteger maxValue, String serverId) throws IOException, ValueOutOfRangeException { // Glue the object which vends/ingests protobuf messages in order to manage state to the network // object which // reads/writes them to the wire in length prefixed form. channelClient = new PaymentChannelClient( wallet, myKey, maxValue, Sha256Hash.create(serverId.getBytes()), new PaymentChannelClient.ClientConnection() { @Override public void sendToServer(Protos.TwoWayChannelMessage msg) { wireParser.write(msg); } @Override public void destroyConnection(PaymentChannelCloseException.CloseReason reason) { channelOpenFuture.setException( new PaymentChannelCloseException( "Payment channel client requested that the connection be closed: " + reason, reason)); wireParser.closeConnection(); } @Override public void channelOpen() { wireParser.setSocketTimeout(0); // Inform the API user that we're done and ready to roll. channelOpenFuture.set(PaymentChannelClientConnection.this); } }); // And glue back in the opposite direction - network to the channelClient. wireParser = new ProtobufParser<Protos.TwoWayChannelMessage>( new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) { try { channelClient.receiveMessage(msg); } catch (ValueOutOfRangeException e) { // We should only get this exception during INITIATE, so channelOpen wasn't called // yet. channelOpenFuture.setException(e); } } @Override public void connectionOpen(ProtobufParser handler) { channelClient.connectionOpen(); } @Override public void connectionClosed(ProtobufParser handler) { channelClient.connectionClosed(); channelOpenFuture.setException( new PaymentChannelCloseException( "The TCP socket died", PaymentChannelCloseException.CloseReason.CONNECTION_CLOSED)); } }, Protos.TwoWayChannelMessage.getDefaultInstance(), Short.MAX_VALUE, timeoutSeconds * 1000); // Initiate the outbound network connection. We don't need to keep this around. The wireParser // object will handle // things from here on out. new NioClient(server, wireParser, timeoutSeconds * 1000); }