/*
* 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.devcoin.protocols.channels;
import com.google.devcoin.core.*;
import com.google.devcoin.store.WalletProtobufSerializer;
import com.google.devcoin.utils.TestWithWallet;
import com.google.devcoin.utils.Threading;
import com.google.devcoin.wallet.WalletFiles;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import org.devcoin.paymentchannel.Protos;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static com.google.devcoin.protocols.channels.PaymentChannelCloseException.CloseReason;
import static org.devcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType;
import static org.junit.Assert.*;
public class ChannelConnectionTest extends TestWithWallet {
private Wallet serverWallet;
private AtomicBoolean fail;
private BlockingQueue<Transaction> broadcasts;
private TransactionBroadcaster mockBroadcaster;
private Semaphore broadcastTxPause;
private static final TransactionBroadcaster failBroadcaster = new TransactionBroadcaster() {
@Override
public ListenableFuture<Transaction> broadcastTransaction(Transaction tx) {
fail();
return null;
}
};
@Before
public void setUp() throws Exception {
super.setUp();
sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN);
sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN);
wallet.addExtension(new StoredPaymentChannelClientStates(wallet, failBroadcaster));
chain = new BlockChain(params, wallet, blockStore); // Recreate chain as sendMoneyToWallet will confuse it
serverWallet = new Wallet(params);
serverWallet.addExtension(new StoredPaymentChannelServerStates(serverWallet, failBroadcaster));
serverWallet.addKey(new ECKey());
chain.addWallet(serverWallet);
// Use an atomic boolean to indicate failure because fail()/assert*() dont work in network threads
fail = new AtomicBoolean(false);
// Set up a way to monitor broadcast transactions. When you expect a broadcast, you must release a permit
// to the broadcastTxPause semaphore so state can be queried in between.
broadcasts = new LinkedBlockingQueue<Transaction>();
broadcastTxPause = new Semaphore(0);
mockBroadcaster = new TransactionBroadcaster() {
@Override
public ListenableFuture<Transaction> broadcastTransaction(Transaction tx) {
broadcastTxPause.acquireUninterruptibly();
SettableFuture<Transaction> future = SettableFuture.create();
future.set(tx);
broadcasts.add(tx);
return future;
}
};
// Because there are no separate threads in the tests here (we call back into client/server in server/client
// handlers), we have lots of lock cycles. A normal user shouldn't have this issue as they are probably not both
// client+server running in the same thread.
Threading.warnOnLockCycles();
}
@After
@Override
public void tearDown() throws Exception {
super.tearDown();
}
@After
public void checkFail() {
assertFalse(fail.get());
Threading.throwOnLockCycles();
}
@Test
public void testSimpleChannel() throws Exception {
// Test with network code and without any issues. We'll broadcast two txns: multisig contract and close transaction.
final SettableFuture<ListenableFuture<PaymentChannelServerState>> serverCloseFuture = SettableFuture.create();
final SettableFuture<Sha256Hash> channelOpenFuture = SettableFuture.create();
final BlockingQueue<BigInteger> q = new LinkedBlockingQueue<BigInteger>();
final PaymentChannelServerListener server = new PaymentChannelServerListener(mockBroadcaster, serverWallet, 1, Utils.COIN,
new PaymentChannelServerListener.HandlerFactory() {
@Nullable
@Override
public ServerConnectionEventHandler onNewConnection(SocketAddress clientAddress) {
return new ServerConnectionEventHandler() {
@Override
public void channelOpen(Sha256Hash channelId) {
channelOpenFuture.set(channelId);
}
@Override
public void paymentIncrease(BigInteger by, BigInteger to) {
q.add(to);
}
@Override
public void channelClosed(CloseReason reason) {
serverCloseFuture.set(null);
}
};
}
});
server.bindAndStart(4243);
PaymentChannelClientConnection client = new PaymentChannelClientConnection(
new InetSocketAddress("localhost", 4243), 1, wallet, myKey, Utils.COIN, "");
// Wait for the multi-sig tx to be transmitted.
broadcastTxPause.release();
Transaction broadcastMultiSig = broadcasts.take();
// Wait for the channel to finish opening.
client.getChannelOpenFuture().get();
assertEquals(broadcastMultiSig.getHash(), channelOpenFuture.get());
// Set up an autosave listener to make sure the server is saving the wallet after each payment increase.
final AtomicInteger autoSaveCount = new AtomicInteger(0);
final CountDownLatch latch = new CountDownLatch(3); // Expect 3 calls.
File tempFile = File.createTempFile("channel_connection_test", ".wallet");
tempFile.deleteOnExit();
serverWallet.autosaveToFile(tempFile, 0, TimeUnit.SECONDS, new WalletFiles.Listener() {
@Override
public void onBeforeAutoSave(File tempFile) {
latch.countDown();
}
@Override
public void onAfterAutoSave(File newlySavedFile) {
}
});
Thread.sleep(1250); // No timeouts once the channel is open
client.incrementPayment(Utils.CENT);
assertEquals(Utils.CENT, q.take());
client.incrementPayment(Utils.CENT);
assertEquals(Utils.CENT.multiply(BigInteger.valueOf(2)), q.take());
client.incrementPayment(Utils.CENT);
assertEquals(Utils.CENT.multiply(BigInteger.valueOf(3)), q.take());
latch.await();
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)serverWallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID);
StoredServerChannel storedServerChannel = channels.getChannel(broadcastMultiSig.getHash());
PaymentChannelServerState serverState = storedServerChannel.getOrCreateState(serverWallet, mockBroadcaster);
// Check that you can call close multiple times with no exceptions.
client.close();
client.close();
broadcastTxPause.release();
broadcasts.take();
assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState());
if (!serverState.getBestValueToMe().equals(Utils.CENT.multiply(BigInteger.valueOf(3))) || !serverState.getFeePaid().equals(BigInteger.ZERO))
fail();
assertTrue(channels.mapChannels.isEmpty());
server.close();
server.close();
}
@Test
public void testServerErrorHandling() throws Exception {
// Gives the server crap and checks proper error responses are sent.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
PaymentChannelServer server = pair.server;
server.connectionOpen();
client.connectionOpen();
// Make sure we get back a BAD_TRANSACTION if we send a bogus refund transaction.
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND);
server.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setType(MessageType.PROVIDE_REFUND)
.setProvideRefund(
Protos.ProvideRefund.newBuilder(msg.getProvideRefund())
.setMultisigKey(ByteString.EMPTY)
.setTx(ByteString.EMPTY)
).build());
final Protos.TwoWayChannelMessage errorMsg = pair.serverRecorder.checkNextMsg(MessageType.ERROR);
assertEquals(Protos.Error.ErrorCode.BAD_TRANSACTION, errorMsg.getError().getCode());
// Make sure the server closes the socket on CLOSE
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
client = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
server = pair.server;
server.connectionOpen();
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.close();
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLOSE));
assertEquals(CloseReason.CLIENT_REQUESTED_CLOSE, pair.serverRecorder.q.take());
// Make sure the server closes the socket on ERROR
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
client = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
server = pair.server;
server.connectionOpen();
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
server.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setType(MessageType.ERROR)
.setError(Protos.Error.newBuilder().setCode(Protos.Error.ErrorCode.TIMEOUT))
.build());
assertEquals(CloseReason.REMOTE_SENT_ERROR, pair.serverRecorder.q.take());
}
@Test
public void testChannelResume() throws Exception {
// Tests various aspects of channel resuming.
Utils.rollMockClock(0);
final Sha256Hash someServerId = Sha256Hash.create(new byte[]{});
// Open up a normal channel.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, someServerId, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
broadcasts.take();
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
Sha256Hash contractHash = (Sha256Hash) pair.serverRecorder.q.take();
pair.clientRecorder.checkOpened();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
// Send a bitcent.
client.incrementPayment(Utils.CENT);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
assertEquals(Utils.CENT, pair.serverRecorder.q.take());
server.close();
server.connectionClosed();
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CLOSE));
client.connectionClosed();
assertFalse(client.connectionOpen);
// There is now an inactive open channel worth COIN-CENT with id Sha256.create(new byte[] {})
StoredPaymentChannelClientStates clientStoredChannels =
(StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
assertEquals(1, clientStoredChannels.mapChannels.size());
assertFalse(clientStoredChannels.mapChannels.values().iterator().next().active);
// Check that server-side won't attempt to reopen a nonexistent channel (it will tell the client to re-initiate
// instead).
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
pair.server.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setType(MessageType.CLIENT_VERSION)
.setClientVersion(Protos.ClientVersion.newBuilder()
.setPreviousChannelContractHash(ByteString.copyFrom(Sha256Hash.create(new byte[]{0x03}).getBytes()))
.setMajor(0).setMinor(42))
.build());
pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION);
pair.serverRecorder.checkNextMsg(MessageType.INITIATE);
// Now reopen/resume the channel after round-tripping the wallets.
wallet = roundTripClientWallet(wallet);
serverWallet = roundTripServerWallet(serverWallet);
clientStoredChannels =
(StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
client = new PaymentChannelClient(wallet, myKey, Utils.COIN, someServerId, pair.clientRecorder);
server = pair.server;
client.connectionOpen();
server.connectionOpen();
// Check the contract hash is sent on the wire correctly.
final Protos.TwoWayChannelMessage clientVersionMsg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
assertTrue(clientVersionMsg.getClientVersion().hasPreviousChannelContractHash());
assertEquals(contractHash, new Sha256Hash(clientVersionMsg.getClientVersion().getPreviousChannelContractHash().toByteArray()));
server.receiveMessage(clientVersionMsg);
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
assertEquals(contractHash, pair.serverRecorder.q.take());
pair.clientRecorder.checkOpened();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
// Send another bitcent and check 2 were received in total.
client.incrementPayment(Utils.CENT);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
pair.serverRecorder.checkTotalPayment(Utils.CENT.multiply(BigInteger.valueOf(2)));
PaymentChannelClient openClient = client;
ChannelTestUtils.RecordingPair openPair = pair;
// Now open up a new client with the same id and make sure the server disconnects the previous client.
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
client = new PaymentChannelClient(wallet, myKey, Utils.COIN, someServerId, pair.clientRecorder);
server = pair.server;
client.connectionOpen();
server.connectionOpen();
// Check that no prev contract hash is sent on the wire the client notices it's already in use by another
// client attached to the same wallet and refuses to resume.
{
Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
assertFalse(msg.getClientVersion().hasPreviousChannelContractHash());
}
// Make sure the server allows two simultaneous opens. It will close the first and allow resumption of the second.
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
client = new PaymentChannelClient(wallet, myKey, Utils.COIN, someServerId, pair.clientRecorder);
server = pair.server;
client.connectionOpen();
server.connectionOpen();
// Swap out the clients version message for a custom one that tries to resume ...
pair.clientRecorder.getNextMsg();
server.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setType(MessageType.CLIENT_VERSION)
.setClientVersion(Protos.ClientVersion.newBuilder()
.setPreviousChannelContractHash(ByteString.copyFrom(contractHash.getBytes()))
.setMajor(0).setMinor(42))
.build());
// We get the usual resume sequence.
pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION);
pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN);
// Verify the previous one was closed.
openPair.serverRecorder.checkNextMsg(MessageType.CLOSE);
assertTrue(clientStoredChannels.getChannel(Sha256Hash.create(new byte[]{}), contractHash).active);
// And finally close the first channel too.
openClient.connectionClosed();
assertFalse(clientStoredChannels.getChannel(Sha256Hash.create(new byte[]{}), contractHash).active);
// Now roll the mock clock and recreate the client object so that it removes the channels and announces refunds.
Utils.rollMockClock(60 * 60 * 24 + 60*5); // Client announces refund 5 minutes after expire time
StoredPaymentChannelClientStates newClientStates = new StoredPaymentChannelClientStates(wallet, mockBroadcaster);
newClientStates.deserializeWalletExtension(wallet, clientStoredChannels.serializeWalletExtension());
broadcastTxPause.release();
assertTrue(broadcasts.take().getOutput(0).getScriptPubKey().isSentToMultiSig());
broadcastTxPause.release();
assertEquals(TransactionConfidence.Source.SELF, broadcasts.take().getConfidence().getSource());
assertTrue(broadcasts.isEmpty());
assertTrue(newClientStates.mapChannels.isEmpty());
// Server also knows it's too late.
StoredPaymentChannelServerStates serverStoredChannels = new StoredPaymentChannelServerStates(serverWallet, mockBroadcaster);
Thread.sleep(2000); // TODO: Fix this stupid hack.
assertTrue(serverStoredChannels.mapChannels.isEmpty());
}
private static Wallet roundTripClientWallet(Wallet wallet) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new WalletProtobufSerializer().writeWallet(wallet, bos);
Wallet wallet2 = new Wallet(wallet.getParams());
wallet2.addExtension(new StoredPaymentChannelClientStates(wallet2, failBroadcaster));
new WalletProtobufSerializer().readWallet(WalletProtobufSerializer.parseToProto(new ByteArrayInputStream(bos.toByteArray())), wallet2);
return wallet2;
}
private static Wallet roundTripServerWallet(Wallet wallet) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new WalletProtobufSerializer().writeWallet(wallet, bos);
Wallet wallet2 = new Wallet(wallet.getParams());
wallet2.addExtension(new StoredPaymentChannelServerStates(wallet2, failBroadcaster));
new WalletProtobufSerializer().readWallet(WalletProtobufSerializer.parseToProto(new ByteArrayInputStream(bos.toByteArray())), wallet2);
return wallet2;
}
@Test
public void testBadResumeHash() throws InterruptedException {
// Check that server-side will reject incorrectly formatted hashes. If anything goes wrong with session resume,
// then the server will start the opening of a new channel automatically, so we expect to see INITIATE here.
ChannelTestUtils.RecordingPair srv =
ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
srv.server.connectionOpen();
srv.server.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setType(MessageType.CLIENT_VERSION)
.setClientVersion(Protos.ClientVersion.newBuilder()
.setPreviousChannelContractHash(ByteString.copyFrom(new byte[]{0x00, 0x01}))
.setMajor(0).setMinor(42))
.build());
srv.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION);
srv.serverRecorder.checkNextMsg(MessageType.INITIATE);
assertTrue(srv.serverRecorder.q.isEmpty());
}
@Test
public void testClientUnknownVersion() throws Exception {
// Tests client rejects unknown version
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
client.connectionOpen();
pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setServerVersion(Protos.ServerVersion.newBuilder().setMajor(2))
.setType(MessageType.SERVER_VERSION).build());
pair.clientRecorder.checkNextMsg(MessageType.ERROR);
assertEquals(CloseReason.NO_ACCEPTABLE_VERSION, pair.clientRecorder.q.take());
// Double-check that we cant do anything that requires an open channel
try {
client.incrementPayment(BigInteger.ONE);
fail();
} catch (IllegalStateException e) { }
}
@Test
public void testClientTimeWindowTooLarge() throws Exception {
// Tests that clients reject too large time windows
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setInitiate(Protos.Initiate.newBuilder().setExpireTimeSecs(Utils.now().getTime() / 1000 + 60 * 60 * 48)
.setMinAcceptedChannelSize(100)
.setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey())))
.setType(MessageType.INITIATE).build());
pair.clientRecorder.checkNextMsg(MessageType.ERROR);
assertEquals(CloseReason.TIME_WINDOW_TOO_LARGE, pair.clientRecorder.q.take());
// Double-check that we cant do anything that requires an open channel
try {
client.incrementPayment(BigInteger.ONE);
fail();
} catch (IllegalStateException e) { }
}
@Test
public void testValuesAreRespected() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setInitiate(Protos.Initiate.newBuilder().setExpireTimeSecs(Utils.now().getTime() / 1000)
.setMinAcceptedChannelSize(Utils.COIN.add(BigInteger.ONE).longValue())
.setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey())))
.setType(MessageType.INITIATE).build());
pair.clientRecorder.checkNextMsg(MessageType.ERROR);
assertEquals(CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE, pair.clientRecorder.q.take());
// Double-check that we cant do anything that requires an open channel
try {
client.incrementPayment(BigInteger.ONE);
fail();
} catch (IllegalStateException e) { }
// Now check that if the server has a lower min size than what we are willing to spend, we do actually open
// a channel of that size.
sendMoneyToWallet(Utils.COIN.multiply(BigInteger.TEN), AbstractBlockChain.NewBlockType.BEST_CHAIN);
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
server = pair.server;
final BigInteger myValue = Utils.COIN.multiply(BigInteger.TEN);
client = new PaymentChannelClient(wallet, myKey, myValue, Sha256Hash.ZERO_HASH, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setInitiate(Protos.Initiate.newBuilder().setExpireTimeSecs(Utils.now().getTime() / 1000)
.setMinAcceptedChannelSize(Utils.COIN.add(BigInteger.ONE).longValue())
.setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey())))
.setType(MessageType.INITIATE).build());
final Protos.TwoWayChannelMessage provideRefund = pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND);
Transaction refund = new Transaction(params, provideRefund.getProvideRefund().getTx().toByteArray());
assertEquals(myValue, refund.getOutput(0).getValue());
}
@Test
public void testClientResumeNothing() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setType(MessageType.CHANNEL_OPEN).build());
pair.clientRecorder.checkNextMsg(MessageType.ERROR);
assertEquals(CloseReason.REMOTE_SENT_INVALID_MESSAGE, pair.clientRecorder.q.take());
}
@Test
public void testClientRandomMessage() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
client.connectionOpen();
pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
// Send a CLIENT_VERSION back to the client - ?!?!!
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setType(MessageType.CLIENT_VERSION).build());
Protos.TwoWayChannelMessage error = pair.clientRecorder.checkNextMsg(MessageType.ERROR);
assertEquals(Protos.Error.ErrorCode.SYNTAX_ERROR, error.getError().getCode());
assertEquals(CloseReason.REMOTE_SENT_INVALID_MESSAGE, pair.clientRecorder.q.take());
}
@Test
public void testDontResumeEmptyChannels() throws Exception {
// Check that if the client has an empty channel that's being kept around in case we need to broadcast the
// refund, we don't accidentally try to resume it).
// Open up a normal channel.
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, Utils.COIN, someServerId, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
broadcasts.take();
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
Sha256Hash contractHash = (Sha256Hash) pair.serverRecorder.q.take();
pair.clientRecorder.checkOpened();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
// Send the whole channel at once.
client.incrementPayment(Utils.COIN);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
// The channel is now empty.
assertEquals(BigInteger.ZERO, client.state().getValueRefunded());
client.connectionClosed();
// Now try opening a new channel with the same server ID and verify the client asks for a new channel.
client = new PaymentChannelClient(wallet, myKey, Utils.COIN, someServerId, pair.clientRecorder);
client.connectionOpen();
Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
assertFalse(msg.getClientVersion().hasPreviousChannelContractHash());
}
}