/* * 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()); } }