/*
* 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 org.bitcoinj.protocols.channels;
import org.bitcoinj.core.*;
import org.bitcoinj.protocols.channels.PaymentChannelClient.VersionSelector;
import org.bitcoinj.testing.TestWithWallet;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.WalletExtension;
import org.bitcoinj.wallet.WalletFiles;
import org.bitcoinj.wallet.WalletProtobufSerializer;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import org.bitcoin.paymentchannel.Protos;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.bitcoinj.core.Coin.*;
import static org.bitcoinj.protocols.channels.PaymentChannelClient.VersionSelector.*;
import static org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason;
import static org.bitcoinj.testing.FakeTxBuilder.createFakeBlock;
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType;
import static org.junit.Assert.*;
@RunWith(Parameterized.class)
public class ChannelConnectionTest extends TestWithWallet {
private static final int CLIENT_MAJOR_VERSION = 1;
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 TransactionBroadcast broadcastTransaction(Transaction tx) {
fail();
return null;
}
};
/**
* We use parameterized tests to run the channel connection tests with each
* version of the channel.
*/
@Parameterized.Parameters(name = "{index}: ChannelConnectionTest({0})")
public static Collection<PaymentChannelClient.DefaultClientChannelProperties> data() {
return Arrays.asList(
new PaymentChannelClient.DefaultClientChannelProperties() {
@Override
public VersionSelector versionSelector() { return VERSION_1;}
},
new PaymentChannelClient.DefaultClientChannelProperties() {
@Override
public VersionSelector versionSelector() { return VERSION_2_ALLOW_1;}
});
}
@Parameterized.Parameter
public IPaymentChannelClient.ClientChannelProperties clientChannelProperties;
/**
* Returns <code>true</code> if we are using a protocol version that requires the exchange of refunds.
*/
private boolean useRefunds() {
return clientChannelProperties.versionSelector() == VERSION_1;
}
/**
* Returns <code>true</code> if the contract being used is a multisig contract
* @return
*/
private boolean isMultiSigContract() {
return clientChannelProperties.versionSelector() == VERSION_1;
}
@Override
@Before
public void setUp() throws Exception {
super.setUp();
Utils.setMockClock(); // Use mock clock
Context.propagate(new Context(PARAMS, 3, Coin.ZERO, false)); // Shorter event horizon for unit tests.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN);
wallet.addExtension(new StoredPaymentChannelClientStates(wallet, failBroadcaster));
serverWallet = new Wallet(PARAMS);
serverWallet.addExtension(new StoredPaymentChannelServerStates(serverWallet, failBroadcaster));
serverWallet.freshReceiveKey();
// 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 TransactionBroadcast broadcastTransaction(Transaction tx) {
broadcastTxPause.acquireUninterruptibly();
SettableFuture<Transaction> future = SettableFuture.create();
future.set(tx);
broadcasts.add(tx);
return TransactionBroadcast.createMockBroadcast(tx, 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();
ECKey.FAKE_SIGNATURES = true;
}
@After
@Override
public void tearDown() throws Exception {
super.tearDown();
ECKey.FAKE_SIGNATURES = false;
}
@After
public void checkFail() {
assertFalse(fail.get());
Threading.throwOnLockCycles();
}
@Test
public void testSimpleChannel() throws Exception {
exectuteSimpleChannelTest(null);
}
@Test
public void testEncryptedClientWallet() throws Exception {
// Encrypt the client wallet
String mySecretPw = "MySecret";
wallet.encrypt(mySecretPw);
KeyParameter userKeySetup = wallet.getKeyCrypter().deriveKey(mySecretPw);
exectuteSimpleChannelTest(userKeySetup);
}
private void exectuteSimpleChannelTest(KeyParameter userKeySetup) throws Exception {
// Test with network code and without any issues. We'll broadcast two txns: multisig contract and settle transaction.
final SettableFuture<ListenableFuture<PaymentChannelV1ServerState>> serverCloseFuture = SettableFuture.create();
final SettableFuture<Sha256Hash> channelOpenFuture = SettableFuture.create();
final BlockingQueue<ChannelTestUtils.UpdatePair> q = new LinkedBlockingQueue<ChannelTestUtils.UpdatePair>();
final PaymentChannelServerListener server = new PaymentChannelServerListener(mockBroadcaster, serverWallet, 30, 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 ListenableFuture<ByteString> paymentIncrease(Coin by, Coin to, ByteString info) {
q.add(new ChannelTestUtils.UpdatePair(to, info));
return Futures.immediateFuture(info);
}
@Override
public void channelClosed(CloseReason reason) {
serverCloseFuture.set(null);
}
};
}
});
server.bindAndStart(4243);
PaymentChannelClientConnection client = new PaymentChannelClientConnection(
new InetSocketAddress("localhost", 4243), 30, wallet, myKey, COIN, "", userKeySetup, clientChannelProperties);
// 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());
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, client.state().getValueSpent());
// Set up an autosave listener to make sure the server is saving the wallet after each payment increase.
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
Coin amount = client.state().getValueSpent();
q.take().assertPair(amount, null);
for (String info : new String[] {null, "one", "two"} ) {
final ByteString bytes = (info==null) ? null :ByteString.copyFromUtf8(info);
final PaymentIncrementAck ack = client.incrementPayment(CENT, bytes, userKeySetup).get();
if (info != null) {
final ByteString ackInfo = ack.getInfo();
assertNotNull("Ack info is null", ackInfo);
assertEquals("Ack info differs ", info, ackInfo.toStringUtf8());
}
amount = amount.add(CENT);
q.take().assertPair(amount, bytes);
}
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 settle multiple times with no exceptions.
client.settle();
client.settle();
broadcastTxPause.release();
Transaction settleTx = broadcasts.take();
assertTrue(serverState.getState() == PaymentChannelServerState.State.CLOSING ||
serverState.getState() == PaymentChannelServerState.State.CLOSED);
// Wait for the server thread to catch up with closing
serverState.close().get();
assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState());
assertEquals(amount, serverState.getBestValueToMe());
assertEquals(ZERO, serverState.getFeePaid());
assertTrue(channels.mapChannels.isEmpty());
// Send the settle TX to the client wallet.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, settleTx);
assertTrue(client.state().getState() == PaymentChannelClientState.State.CLOSED);
server.close();
server.close();
// Now confirm the settle TX and see if the channel deletes itself from the wallet.
assertEquals(1, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size());
wallet.notifyNewBestBlock(createFakeBlock(blockStore, Block.BLOCK_HEIGHT_GENESIS).storedBlock);
assertEquals(1, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size());
wallet.notifyNewBestBlock(createFakeBlock(blockStore, Block.BLOCK_HEIGHT_GENESIS + 1).storedBlock);
assertEquals(0, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size());
}
@Test
public void testServerErrorHandling_badTransaction() throws Exception {
if (!useRefunds()) {
// This test only applies to versions with refunds
return;
}
// Gives the server crap and checks proper error responses are sent.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, 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());
}
@Test
public void testServerErrorHandling_killSocketOnClose() throws Exception {
// Make sure the server closes the socket on CLOSE
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
server.connectionOpen();
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.settle();
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLOSE));
assertEquals(CloseReason.CLIENT_REQUESTED_CLOSE, pair.serverRecorder.q.take());
}
@Test
public void testServerErrorHandling_killSocketOnError() throws Exception {
// Make sure the server closes the socket on ERROR
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer 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 testClientErrorHandlingIncreasePaymentError() throws Exception {
// Tests various aspects of channel resuming.
Utils.setMockClock();
final Sha256Hash someServerId = Sha256Hash.of(new byte[]{});
// Open up a normal channel.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
final Protos.TwoWayChannelMessage initiateMsg = pair.serverRecorder.checkNextMsg(MessageType.INITIATE);
Coin minPayment = Coin.valueOf(initiateMsg.getInitiate().getMinPayment());
client.receiveMessage(initiateMsg);
if (useRefunds()) {
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();
pair.serverRecorder.checkTotalPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
Sha256Hash contractHash = (Sha256Hash) pair.serverRecorder.q.take();
pair.clientRecorder.checkInitiated();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
assertEquals(minPayment, client.state().getValueSpent());
// Send a bitcent.
Coin amount = minPayment.add(CENT);
ListenableFuture<PaymentIncrementAck> ackFuture = client.incrementPayment(CENT);
// We never pass this message to the server
// Instead we pretend the server didn't like our increase
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setType(MessageType.ERROR)
.setError(Protos.Error.newBuilder().setCode(Protos.Error.ErrorCode.CHANNEL_VALUE_TOO_LARGE)) // some random error
.build());
// Now we need the client to actually close the future and report this error
try {
ackFuture.get(1L, TimeUnit.SECONDS);
fail("This should not work");
} catch (ExecutionException ee) {
PaymentChannelCloseException ce = (PaymentChannelCloseException) ee.getCause();
assertEquals(CloseReason.REMOTE_SENT_ERROR, ce.getCloseReason());
} catch (TimeoutException e) {
fail("Should not time out");
}
}
@Test
public void testChannelResume() throws Exception {
// Tests various aspects of channel resuming.
Utils.setMockClock();
final Sha256Hash someServerId = Sha256Hash.of(new byte[]{});
// Open up a normal channel.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
final Protos.TwoWayChannelMessage initiateMsg = pair.serverRecorder.checkNextMsg(MessageType.INITIATE);
Coin minPayment = Coin.valueOf(initiateMsg.getInitiate().getMinPayment());
client.receiveMessage(initiateMsg);
if (useRefunds()) {
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();
pair.serverRecorder.checkTotalPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
Sha256Hash contractHash = (Sha256Hash) pair.serverRecorder.q.take();
pair.clientRecorder.checkInitiated();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
assertEquals(minPayment, client.state().getValueSpent());
// Send a bitcent.
Coin amount = minPayment.add(CENT);
client.incrementPayment(CENT);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
assertEquals(amount, ((ChannelTestUtils.UpdatePair)pair.serverRecorder.q.take()).amount);
server.close();
server.connectionClosed();
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.PAYMENT_ACK));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CLOSE));
client.connectionClosed();
assertFalse(client.connectionOpen);
// There is now an inactive open channel worth COIN-CENT + minPayment 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.hash(new byte[] { 0x03 })))
.setMajor(CLIENT_MAJOR_VERSION).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, COIN, someServerId, null, clientChannelProperties, 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, Sha256Hash.wrap(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(CENT);
amount = amount.add(CENT);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
pair.serverRecorder.checkTotalPayment(amount);
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.PAYMENT_ACK));
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, COIN, someServerId, null, clientChannelProperties, 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, COIN, someServerId, null, clientChannelProperties, 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(CLIENT_MAJOR_VERSION).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(someServerId, contractHash).active);
// And finally close the first channel too.
openClient.connectionClosed();
assertFalse(clientStoredChannels.getChannel(someServerId, contractHash).active);
// Now roll the mock clock and recreate the client object so that it removes the channels and announces refunds.
assertEquals(86640, clientStoredChannels.getSecondsUntilExpiry(someServerId));
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();
if (isMultiSigContract()) {
assertTrue(broadcasts.take().getOutput(0).getScriptPubKey().isSentToMultiSig());
} else {
assertTrue(broadcasts.take().getOutput(0).getScriptPubKey().isPayToScriptHash());
}
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);
org.bitcoinj.wallet.Protos.Wallet proto = WalletProtobufSerializer.parseToProto(new ByteArrayInputStream(bos.toByteArray()));
StoredPaymentChannelClientStates state = new StoredPaymentChannelClientStates(null, failBroadcaster);
return new WalletProtobufSerializer().readWallet(wallet.getParams(), new WalletExtension[] { state }, proto);
}
private static Wallet roundTripServerWallet(Wallet wallet) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new WalletProtobufSerializer().writeWallet(wallet, bos);
StoredPaymentChannelServerStates state = new StoredPaymentChannelServerStates(null, failBroadcaster);
org.bitcoinj.wallet.Protos.Wallet proto = WalletProtobufSerializer.parseToProto(new ByteArrayInputStream(bos.toByteArray()));
return new WalletProtobufSerializer().readWallet(wallet.getParams(), new WalletExtension[] { state }, proto);
}
@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(CLIENT_MAJOR_VERSION).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, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setServerVersion(Protos.ServerVersion.newBuilder().setMajor(-1))
.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(Coin.SATOSHI);
fail();
} catch (IllegalStateException e) { }
}
@Test
public void testClientTimeWindowUnacceptable() throws Exception {
// Tests that clients reject too large time windows
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster, 100);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, 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.currentTimeSeconds() + 60 * 60 * 48)
.setMinAcceptedChannelSize(100)
.setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey()))
.setMinPayment(Transaction.MIN_NONDUST_OUTPUT.value))
.setType(MessageType.INITIATE).build());
pair.clientRecorder.checkNextMsg(MessageType.ERROR);
assertEquals(CloseReason.TIME_WINDOW_UNACCEPTABLE, pair.clientRecorder.q.take());
// Double-check that we cant do anything that requires an open channel
try {
client.incrementPayment(Coin.SATOSHI);
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, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, 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.currentTimeSeconds())
.setMinAcceptedChannelSize(COIN.add(SATOSHI).value)
.setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey()))
.setMinPayment(Transaction.MIN_NONDUST_OUTPUT.value))
.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(Coin.SATOSHI);
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(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN.multiply(10));
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
server = pair.server;
final Coin myValue = COIN.multiply(10);
client = new PaymentChannelClient(wallet, myKey, myValue, Sha256Hash.ZERO_HASH, null, clientChannelProperties, 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.currentTimeSeconds())
.setMinAcceptedChannelSize(COIN.add(SATOSHI).value)
.setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey()))
.setMinPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value))
.setType(MessageType.INITIATE).build());
if (useRefunds()) {
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());
} else {
assertEquals(2, client.state().getMajorVersion());
PaymentChannelV2ClientState state = (PaymentChannelV2ClientState) client.state();
assertEquals(myValue, state.refundTx.getOutput(0).getValue());
}
}
@Test
public void testEmptyWallet() throws Exception {
Wallet emptyWallet = new Wallet(PARAMS);
emptyWallet.freshReceiveKey();
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(emptyWallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
try {
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
.setInitiate(Protos.Initiate.newBuilder().setExpireTimeSecs(Utils.currentTimeSeconds())
.setMinAcceptedChannelSize(CENT.value)
.setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey()))
.setMinPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value))
.setType(MessageType.INITIATE).build());
fail();
} catch (InsufficientMoneyException expected) {
// This should be thrown.
}
}
@Test
public void testClientRefusesNonCanonicalKey() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
Protos.TwoWayChannelMessage.Builder initiateMsg = Protos.TwoWayChannelMessage.newBuilder(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
ByteString brokenKey = initiateMsg.getInitiate().getMultisigKey();
brokenKey = ByteString.copyFrom(Arrays.copyOf(brokenKey.toByteArray(), brokenKey.size() + 1));
initiateMsg.getInitiateBuilder().setMultisigKey(brokenKey);
client.receiveMessage(initiateMsg.build());
pair.clientRecorder.checkNextMsg(MessageType.ERROR);
assertEquals(CloseReason.REMOTE_SENT_INVALID_MESSAGE, pair.clientRecorder.q.take());
}
@Test
public void testClientResumeNothing() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server;
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, 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, COIN, Sha256Hash.ZERO_HASH, null, clientChannelProperties, 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, COIN, someServerId, null, clientChannelProperties, 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));
if (useRefunds()) {
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();
pair.serverRecorder.checkTotalPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
Sha256Hash contractHash = (Sha256Hash) pair.serverRecorder.q.take();
pair.clientRecorder.checkInitiated();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
// Send the whole channel at once. The server will broadcast the final contract and settle the channel for us.
client.incrementPayment(client.state().getValueRefunded());
broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
broadcasts.take();
// The channel is now empty.
assertEquals(Coin.ZERO, client.state().getValueRefunded());
pair.serverRecorder.q.take(); // Take the Coin.
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.PAYMENT_ACK));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CLOSE));
assertEquals(CloseReason.SERVER_REQUESTED_CLOSE, pair.clientRecorder.q.take());
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, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
client.connectionOpen();
Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
assertFalse(msg.getClientVersion().hasPreviousChannelContractHash());
}
@Test
public void repeatedChannels() throws Exception {
// Ensures we're selecting channels correctly. Covers a bug in which we'd always try and fail to resume
// the first channel due to lack of proper closing behaviour.
// Open up a normal channel, but don't spend all of it, then settle it.
{
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, 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));
if (useRefunds()) {
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();
pair.serverRecorder.checkTotalPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
Sha256Hash contractHash = (Sha256Hash) pair.serverRecorder.q.take();
pair.clientRecorder.checkInitiated();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
for (int i = 0; i < 3; i++) {
ListenableFuture<PaymentIncrementAck> future = client.incrementPayment(CENT);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
pair.serverRecorder.q.take();
final Protos.TwoWayChannelMessage msg = pair.serverRecorder.checkNextMsg(MessageType.PAYMENT_ACK);
final Protos.PaymentAck paymentAck = msg.getPaymentAck();
assertTrue("No PaymentAck.Info", paymentAck.hasInfo());
assertEquals("Wrong PaymentAck info", ByteString.copyFromUtf8(CENT.toPlainString()), paymentAck.getInfo());
client.receiveMessage(msg);
assertTrue(future.isDone());
final PaymentIncrementAck paymentIncrementAck = future.get();
assertEquals("Wrong value returned from increasePayment", CENT, paymentIncrementAck.getValue());
assertEquals("Wrong info returned from increasePayment", ByteString.copyFromUtf8(CENT.toPlainString()), paymentIncrementAck.getInfo());
}
// Settle it and verify it's considered to be settled.
broadcastTxPause.release();
client.settle();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLOSE));
Transaction settlement1 = broadcasts.take();
// Server sends back the settle TX it just broadcast.
final Protos.TwoWayChannelMessage closeMsg = pair.serverRecorder.checkNextMsg(MessageType.CLOSE);
final Transaction settlement2 = new Transaction(PARAMS, closeMsg.getSettlement().getTx().toByteArray());
assertEquals(settlement1, settlement2);
client.receiveMessage(closeMsg);
assertNotNull(wallet.getTransaction(settlement2.getHash())); // Close TX entered the wallet.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, settlement1);
client.connectionClosed();
server.connectionClosed();
}
// Now open a second channel and don't spend all of it/don't settle it.
{
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, pair.clientRecorder);
PaymentChannelServer server = pair.server;
client.connectionOpen();
final Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
assertFalse(msg.getClientVersion().hasPreviousChannelContractHash());
server.receiveMessage(msg);
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
if (useRefunds()) {
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();
pair.serverRecorder.checkTotalPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.CHANNEL_OPEN));
Sha256Hash contractHash = (Sha256Hash) pair.serverRecorder.q.take();
pair.clientRecorder.checkInitiated();
assertNull(pair.serverRecorder.q.poll());
assertNull(pair.clientRecorder.q.poll());
client.incrementPayment(CENT);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT));
client.connectionClosed();
server.connectionClosed();
}
// Now connect again and check we resume the second channel.
{
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen();
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, null, clientChannelProperties, 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.CHANNEL_OPEN));
}
assertEquals(2, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size());
}
}