diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/ClientState.java b/core/src/main/java/com/google/bitcoin/protocols/channels/ClientState.java index abb8bf00..4045169e 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/channels/ClientState.java +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/ClientState.java @@ -764,6 +764,28 @@ public final class ClientState { * required uint64 refundFees = 6; */ long getRefundFees(); + + // optional bytes closeTransactionHash = 7; + /** + * optional bytes closeTransactionHash = 7; + * + *
+     * When set, the hash of the transaction that was presented by the server for closure of the channel.
+     * It spends the contractTransaction and is expected to be broadcast to the network by the server.
+     * It's supposed to be in the wallet already.
+     * 
+ */ + boolean hasCloseTransactionHash(); + /** + * optional bytes closeTransactionHash = 7; + * + *
+     * When set, the hash of the transaction that was presented by the server for closure of the channel.
+     * It spends the contractTransaction and is expected to be broadcast to the network by the server.
+     * It's supposed to be in the wallet already.
+     * 
+ */ + com.google.protobuf.ByteString getCloseTransactionHash(); } /** * Protobuf type {@code paymentchannels.StoredClientPaymentChannel} @@ -851,6 +873,11 @@ public final class ClientState { refundFees_ = input.readUInt64(); break; } + case 58: { + bitField0_ |= 0x00000040; + closeTransactionHash_ = input.readBytes(); + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -987,6 +1014,34 @@ public final class ClientState { return refundFees_; } + // optional bytes closeTransactionHash = 7; + public static final int CLOSETRANSACTIONHASH_FIELD_NUMBER = 7; + private com.google.protobuf.ByteString closeTransactionHash_; + /** + * optional bytes closeTransactionHash = 7; + * + *
+     * When set, the hash of the transaction that was presented by the server for closure of the channel.
+     * It spends the contractTransaction and is expected to be broadcast to the network by the server.
+     * It's supposed to be in the wallet already.
+     * 
+ */ + public boolean hasCloseTransactionHash() { + return ((bitField0_ & 0x00000040) == 0x00000040); + } + /** + * optional bytes closeTransactionHash = 7; + * + *
+     * When set, the hash of the transaction that was presented by the server for closure of the channel.
+     * It spends the contractTransaction and is expected to be broadcast to the network by the server.
+     * It's supposed to be in the wallet already.
+     * 
+ */ + public com.google.protobuf.ByteString getCloseTransactionHash() { + return closeTransactionHash_; + } + private void initFields() { id_ = com.google.protobuf.ByteString.EMPTY; contractTransaction_ = com.google.protobuf.ByteString.EMPTY; @@ -994,6 +1049,7 @@ public final class ClientState { myKey_ = com.google.protobuf.ByteString.EMPTY; valueToMe_ = 0L; refundFees_ = 0L; + closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -1049,6 +1105,9 @@ public final class ClientState { if (((bitField0_ & 0x00000020) == 0x00000020)) { output.writeUInt64(6, refundFees_); } + if (((bitField0_ & 0x00000040) == 0x00000040)) { + output.writeBytes(7, closeTransactionHash_); + } getUnknownFields().writeTo(output); } @@ -1082,6 +1141,10 @@ public final class ClientState { size += com.google.protobuf.CodedOutputStream .computeUInt64Size(6, refundFees_); } + if (((bitField0_ & 0x00000040) == 0x00000040)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(7, closeTransactionHash_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -1215,6 +1278,8 @@ public final class ClientState { bitField0_ = (bitField0_ & ~0x00000010); refundFees_ = 0L; bitField0_ = (bitField0_ & ~0x00000020); + closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000040); return this; } @@ -1267,6 +1332,10 @@ public final class ClientState { to_bitField0_ |= 0x00000020; } result.refundFees_ = refundFees_; + if (((from_bitField0_ & 0x00000040) == 0x00000040)) { + to_bitField0_ |= 0x00000040; + } + result.closeTransactionHash_ = closeTransactionHash_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -1301,6 +1370,9 @@ public final class ClientState { if (other.hasRefundFees()) { setRefundFees(other.getRefundFees()); } + if (other.hasCloseTransactionHash()) { + setCloseTransactionHash(other.getCloseTransactionHash()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -1562,6 +1634,66 @@ public final class ClientState { return this; } + // optional bytes closeTransactionHash = 7; + private com.google.protobuf.ByteString closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY; + /** + * optional bytes closeTransactionHash = 7; + * + *
+       * When set, the hash of the transaction that was presented by the server for closure of the channel.
+       * It spends the contractTransaction and is expected to be broadcast to the network by the server.
+       * It's supposed to be in the wallet already.
+       * 
+ */ + public boolean hasCloseTransactionHash() { + return ((bitField0_ & 0x00000040) == 0x00000040); + } + /** + * optional bytes closeTransactionHash = 7; + * + *
+       * When set, the hash of the transaction that was presented by the server for closure of the channel.
+       * It spends the contractTransaction and is expected to be broadcast to the network by the server.
+       * It's supposed to be in the wallet already.
+       * 
+ */ + public com.google.protobuf.ByteString getCloseTransactionHash() { + return closeTransactionHash_; + } + /** + * optional bytes closeTransactionHash = 7; + * + *
+       * When set, the hash of the transaction that was presented by the server for closure of the channel.
+       * It spends the contractTransaction and is expected to be broadcast to the network by the server.
+       * It's supposed to be in the wallet already.
+       * 
+ */ + public Builder setCloseTransactionHash(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000040; + closeTransactionHash_ = value; + onChanged(); + return this; + } + /** + * optional bytes closeTransactionHash = 7; + * + *
+       * When set, the hash of the transaction that was presented by the server for closure of the channel.
+       * It spends the contractTransaction and is expected to be broadcast to the network by the server.
+       * It's supposed to be in the wallet already.
+       * 
+ */ + public Builder clearCloseTransactionHash() { + bitField0_ = (bitField0_ & ~0x00000040); + closeTransactionHash_ = getDefaultInstance().getCloseTransactionHash(); + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:paymentchannels.StoredClientPaymentChannel) } @@ -1595,12 +1727,13 @@ public final class ClientState { "\n storedclientpaymentchannel.proto\022\017paym" + "entchannels\"\\\n\033StoredClientPaymentChanne" + "ls\022=\n\010channels\030\001 \003(\0132+.paymentchannels.S" + - "toredClientPaymentChannel\"\226\001\n\032StoredClie" + + "toredClientPaymentChannel\"\264\001\n\032StoredClie" + "ntPaymentChannel\022\n\n\002id\030\001 \002(\014\022\033\n\023contract" + "Transaction\030\002 \002(\014\022\031\n\021refundTransaction\030\003" + " \002(\014\022\r\n\005myKey\030\004 \002(\014\022\021\n\tvalueToMe\030\005 \002(\004\022\022" + - "\n\nrefundFees\030\006 \002(\004B4\n%com.google.bitcoin" + - ".protocols.channelsB\013ClientState" + "\n\nrefundFees\030\006 \002(\004\022\034\n\024closeTransactionHa" + + "sh\030\007 \001(\014B4\n%com.google.bitcoin.protocols" + + ".channelsB\013ClientState" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -1618,7 +1751,7 @@ public final class ClientState { internal_static_paymentchannels_StoredClientPaymentChannel_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_paymentchannels_StoredClientPaymentChannel_descriptor, - new java.lang.String[] { "Id", "ContractTransaction", "RefundTransaction", "MyKey", "ValueToMe", "RefundFees", }); + new java.lang.String[] { "Id", "ContractTransaction", "RefundTransaction", "MyKey", "ValueToMe", "RefundFees", "CloseTransactionHash", }); return null; } }; diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java index af9b7abf..dd5b3c5e 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClientState.java @@ -20,6 +20,7 @@ import com.google.bitcoin.core.*; import com.google.bitcoin.crypto.TransactionSignature; import com.google.bitcoin.script.Script; import com.google.bitcoin.script.ScriptBuilder; +import com.google.bitcoin.utils.Threading; import com.google.bitcoin.wallet.AllowUnconfirmedCoinSelector; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -64,6 +65,7 @@ import static com.google.common.base.Preconditions.*; */ public class PaymentChannelClientState { private static final Logger log = LoggerFactory.getLogger(PaymentChannelClientState.class); + private static final int CONFIRMATIONS_FOR_DELETE = 3; private final Wallet wallet; // Both sides need a key (private in our case, public for the server) in order to manage the multisig contract @@ -97,7 +99,8 @@ public class PaymentChannelClientState { SAVE_STATE_IN_WALLET, PROVIDE_MULTISIG_CONTRACT_TO_SERVER, READY, - EXPIRED + EXPIRED, + CLOSED } private State state; @@ -118,6 +121,16 @@ public class PaymentChannelClientState { this.valueToMe = checkNotNull(storedClientChannel.valueToMe); this.storedChannel = storedClientChannel; this.state = State.READY; + initWalletListeners(); + } + + private boolean isCloseTransaction(Transaction tx) { + try { + tx.getInput(0).verify(multisigContract.getOutput(0)); + return true; + } catch (VerificationException e) { + return false; + } } /** @@ -139,6 +152,7 @@ public class PaymentChannelClientState { BigInteger value, long expiryTimeInSeconds) throws VerificationException { checkArgument(value.compareTo(BigInteger.ZERO) > 0); this.wallet = checkNotNull(wallet); + initWalletListeners(); this.serverMultisigKey = checkNotNull(serverMultisigKey); if (!myKey.isPubKeyCanonical() || !serverMultisigKey.isPubKeyCanonical()) throw new VerificationException("Pubkey was not canonical (ie non-standard)"); @@ -148,6 +162,51 @@ public class PaymentChannelClientState { this.state = State.NEW; } + private synchronized void initWalletListeners() { + // Register a listener that watches out for the server closing the channel. + if (storedChannel != null && storedChannel.close != null) { + watchCloseConfirmations(); + } + wallet.addEventListener(new AbstractWalletEventListener() { + @Override + public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { + synchronized (PaymentChannelClientState.this) { + if (multisigContract == null) return; + if (isCloseTransaction(tx)) { + log.info("Close: transaction {} closed contract {}", tx.getHash(), multisigContract.getHash()); + // Record the fact that it was closed along with the transaction that closed it. + state = State.CLOSED; + if (storedChannel == null) return; + storedChannel.close = tx; + updateChannelInWallet(); + watchCloseConfirmations(); + } + } + } + }, Threading.SAME_THREAD); + } + + private void watchCloseConfirmations() { + // When we see the close transaction get a few confirmations, we can just delete the record + // of this channel along with the refund tx from the wallet, because we're not going to need + // any of that any more. + storedChannel.close.getConfidence().getDepthFuture(CONFIRMATIONS_FOR_DELETE).addListener(new Runnable() { + @Override + public void run() { + deleteChannelFromWallet(); + } + }, Threading.SAME_THREAD); + } + + private synchronized void deleteChannelFromWallet() { + log.info("Close tx has confirmed, deleting channel from wallet: {}", storedChannel); + StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates) + wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); + channels.removeChannel(storedChannel); + wallet.addOrUpdateExtension(channels); + storedChannel = null; + } + /** * This object implements a state machine, and this accessor returns which state it's currently in. */ @@ -359,7 +418,6 @@ public class PaymentChannelClientState { synchronized (storedChannel) { storedChannel.active = false; } - storedChannel = null; } /** diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelClientStates.java b/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelClientStates.java index 480ae92b..784c2a4f 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelClientStates.java +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelClientStates.java @@ -22,6 +22,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.HashMultimap; import com.google.protobuf.ByteString; import net.jcip.annotations.GuardedBy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.math.BigInteger; @@ -39,6 +41,7 @@ import static com.google.common.base.Preconditions.checkState; * and broadcasting the refund transaction over the given {@link TransactionBroadcaster}. */ public class StoredPaymentChannelClientStates implements WalletExtension { + private static final Logger log = LoggerFactory.getLogger(StoredPaymentChannelClientStates.class); static final String EXTENSION_ID = StoredPaymentChannelClientStates.class.getName(); @GuardedBy("lock") @VisibleForTesting final HashMultimap mapChannels = HashMultimap.create(); @@ -59,6 +62,30 @@ public class StoredPaymentChannelClientStates implements WalletExtension { this.containingWallet = checkNotNull(containingWallet); } + /** Returns this extension from the given wallet, or null if no such extension was added. */ + @Nullable + public static StoredPaymentChannelClientStates getFromWallet(Wallet wallet) { + return (StoredPaymentChannelClientStates) wallet.getExtensions().get(EXTENSION_ID); + } + + /** Returns the outstanding amount of money sent back to us for all channels to this server added together. */ + public BigInteger getBalanceForServer(Sha256Hash id) { + BigInteger balance = BigInteger.ZERO; + lock.lock(); + try { + Set setChannels = mapChannels.get(id); + for (StoredClientChannel channel : setChannels) { + synchronized (channel) { + if (channel.close != null) continue; + balance = balance.add(channel.valueToMe); + } + } + return balance; + } finally { + lock.unlock(); + } + } + /** * Finds an inactive channel with the given id and returns it, or returns null. */ @@ -70,12 +97,17 @@ public class StoredPaymentChannelClientStates implements WalletExtension { for (StoredClientChannel channel : setChannels) { synchronized (channel) { // Check if the channel is usable (has money, inactive) and if so, activate it. - if (channel.valueToMe.equals(BigInteger.ZERO)) + log.info("Considering channel {} contract {}", channel.hashCode(), channel.contract.getHash()); + if (channel.close != null || channel.valueToMe.equals(BigInteger.ZERO)) { + log.info(" ... but is closed or empty"); continue; + } if (!channel.active) { + log.info(" ... activating"); channel.active = true; return channel; } + log.info(" ... but is already active"); } } } finally { @@ -169,13 +201,16 @@ public class StoredPaymentChannelClientStates implements WalletExtension { checkState(channel.refundFees.compareTo(BigInteger.ZERO) >= 0 && channel.refundFees.compareTo(NetworkParameters.MAX_MONEY) < 0); checkNotNull(channel.myKey.getPrivKeyBytes()); checkState(channel.refund.getConfidence().getSource() == TransactionConfidence.Source.SELF); - builder.addChannels(ClientState.StoredClientPaymentChannel.newBuilder() + final ClientState.StoredClientPaymentChannel.Builder value = ClientState.StoredClientPaymentChannel.newBuilder() .setId(ByteString.copyFrom(channel.id.getBytes())) .setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize())) .setRefundTransaction(ByteString.copyFrom(channel.refund.bitcoinSerialize())) .setMyKey(ByteString.copyFrom(channel.myKey.getPrivKeyBytes())) .setValueToMe(channel.valueToMe.longValue()) - .setRefundFees(channel.refundFees.longValue())); + .setRefundFees(channel.refundFees.longValue()); + if (channel.close != null) + value.setCloseTransactionHash(ByteString.copyFrom(channel.close.getHash().getBytes())); + builder.addChannels(value); } return builder.build().toByteArray(); } finally { @@ -200,6 +235,8 @@ public class StoredPaymentChannelClientStates implements WalletExtension { new ECKey(new BigInteger(1, storedState.getMyKey().toByteArray()), null, true), BigInteger.valueOf(storedState.getValueToMe()), BigInteger.valueOf(storedState.getRefundFees()), false); + if (storedState.hasCloseTransactionHash()) + channel.close = containingWallet.getTransaction(new Sha256Hash(storedState.toByteArray())); putChannel(channel, false); } } finally { @@ -229,6 +266,8 @@ public class StoredPaymentChannelClientStates implements WalletExtension { class StoredClientChannel { Sha256Hash id; Transaction contract, refund; + // The transaction that closed the channel (generated by the server) + Transaction close; ECKey myKey; BigInteger valueToMe, refundFees; @@ -249,14 +288,17 @@ class StoredClientChannel { @Override public String toString() { final String newline = String.format("%n"); + final String closeStr = close == null ? "still open" : close.toString().replaceAll(newline, newline + " "); return String.format("Stored client channel for server ID %s (%s)%n" + - " Key: %s%n" + - " Value left: %d%n" + - " Refund fees: %d%n" + - " Contract: %s" + - "Refund: %s", + " Key: %s%n" + + " Value left: %d%n" + + " Refund fees: %d%n" + + " Contract: %s" + + "Refund: %s" + + "Close: %s", id, active ? "active" : "inactive", myKey, valueToMe, refundFees, contract.toString().replaceAll(newline, newline + " "), - refund.toString().replaceAll(newline, newline + " ")); + refund.toString().replaceAll(newline, newline + " "), + closeStr); } } diff --git a/core/src/storedclientpaymentchannel.proto b/core/src/storedclientpaymentchannel.proto index 2bbbe7e4..6e31409c 100644 --- a/core/src/storedclientpaymentchannel.proto +++ b/core/src/storedclientpaymentchannel.proto @@ -42,4 +42,8 @@ message StoredClientPaymentChannel { required bytes myKey = 4; required uint64 valueToMe = 5; required uint64 refundFees = 6; + // When set, the hash of the transaction that was presented by the server for closure of the channel. + // It spends the contractTransaction and is expected to be broadcast to the network by the server. + // It's supposed to be in the wallet already. + optional bytes closeTransactionHash = 7; } \ No newline at end of file diff --git a/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java b/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java index 0680b4ae..ddaac751 100644 --- a/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java +++ b/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java @@ -41,11 +41,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static com.google.bitcoin.protocols.channels.PaymentChannelCloseException.CloseReason; +import static com.google.bitcoin.utils.TestUtils.createFakeBlock; import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType; import static org.junit.Assert.*; public class ChannelConnectionTest extends TestWithWallet { private Wallet serverWallet; + private BlockChain serverChain; private AtomicBoolean fail; private BlockingQueue broadcasts; private TransactionBroadcaster mockBroadcaster; @@ -65,11 +67,10 @@ public class ChannelConnectionTest extends TestWithWallet { 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); + serverChain = new BlockChain(params, serverWallet, blockStore); // Use an atomic boolean to indicate failure because fail()/assert*() dont work in network threads fail = new AtomicBoolean(false); @@ -181,16 +182,27 @@ public class ChannelConnectionTest extends TestWithWallet { client.close(); broadcastTxPause.release(); - broadcasts.take(); + Transaction closeTx = 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()); + // Send the close TX to the client wallet. + sendMoneyToWallet(closeTx, AbstractBlockChain.NewBlockType.BEST_CHAIN); + assertEquals(PaymentChannelClientState.State.CLOSED, client.state().getState()); + server.close(); server.close(); + + // Now confirm the close TX and see if the channel deletes itself from the wallet. + assertEquals(1, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size()); + wallet.notifyNewBestBlock(createFakeBlock(blockStore).storedBlock); + assertEquals(1, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size()); + wallet.notifyNewBestBlock(createFakeBlock(blockStore).storedBlock); + assertEquals(1, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size()); + wallet.notifyNewBestBlock(createFakeBlock(blockStore).storedBlock); + assertEquals(0, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size()); } @Test @@ -602,4 +614,87 @@ public class ChannelConnectionTest extends TestWithWallet { 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 close it. + { + 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()); + client.incrementPayment(Utils.CENT); + client.incrementPayment(Utils.CENT); + client.incrementPayment(Utils.CENT); + server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT)); + server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT)); + server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT)); + // Close it and verify it's considered to be closed. + broadcastTxPause.release(); + client.close(); + server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLOSE)); + Transaction close = broadcasts.take(); + sendMoneyToWallet(close, AbstractBlockChain.NewBlockType.BEST_CHAIN); + client.connectionClosed(); + server.connectionClosed(); + } + // Now open a second channel and don't spend all of it/don't close it. + { + 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(); + 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)); + 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()); + client.incrementPayment(Utils.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, 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.CHANNEL_OPEN)); + } + assertEquals(2, StoredPaymentChannelClientStates.getFromWallet(wallet).mapChannels.size()); + } }