From 6342af0913f2f5583b57c31ffef780cc8c071284 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 4 Oct 2013 17:10:42 +0200 Subject: [PATCH] Payment channels: protocol tweak - when the client sends a CLOSE, the server sends a CLOSE back that contains the final negotiated contract, so it can be inserted into the wallet without needing to wait for a network broadcast (this is useful if the client does not have internet connectivity at that point). --- .../channels/PaymentChannelClient.java | 29 +- .../channels/PaymentChannelClientState.java | 6 +- .../channels/PaymentChannelServer.java | 36 +- .../channels/PaymentChannelServerState.java | 19 +- .../StoredPaymentChannelServerStates.java | 1 - .../org/bitcoin/paymentchannel/Protos.java | 773 ++++++++++++++++-- core/src/paymentchannel.proto | 22 +- .../channels/ChannelConnectionTest.java | 10 + .../channels/PaymentChannelStateTest.java | 25 +- 9 files changed, 830 insertions(+), 91 deletions(-) diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClient.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClient.java index bfc9c17b..cb5b26f6 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClient.java +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelClient.java @@ -91,7 +91,9 @@ public class PaymentChannelClient { WAITING_FOR_INITIATE, WAITING_FOR_REFUND_RETURN, WAITING_FOR_CHANNEL_OPEN, - CHANNEL_OPEN + CHANNEL_OPEN, + WAITING_FOR_CHANNEL_CLOSE, + CHANNEL_CLOSED, } @GuardedBy("lock") private InitStep step = InitStep.WAITING_FOR_CONNECTION_OPEN; @@ -253,7 +255,7 @@ public class PaymentChannelClient { receiveChannelOpen(); return; case CLOSE: - conn.destroyConnection(CloseReason.SERVER_REQUESTED_CLOSE); + receiveClose(msg); return; case ERROR: checkState(msg.hasError()); @@ -290,6 +292,26 @@ public class PaymentChannelClient { } } + @GuardedBy("lock") + private void receiveClose(Protos.TwoWayChannelMessage msg) throws VerificationException { + checkState(lock.isHeldByCurrentThread()); + if (msg.hasClose()) { + Transaction closeTx = new Transaction(wallet.getParams(), msg.getClose().getTx().toByteArray()); + // TODO: set source + if (state != null && state().isCloseTransaction(closeTx)) { + // The wallet has a listener on it that the state object will use to do the right thing at this + // point (like watching it for confirmations). The tx has been checked by now for syntactical validity + // and that it correctly spends the multisig contract. + wallet.receivePending(closeTx, null); + } + } + if (step == InitStep.WAITING_FOR_CHANNEL_CLOSE) + conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE); + else + conn.destroyConnection(CloseReason.SERVER_REQUESTED_CLOSE); + step = InitStep.CHANNEL_CLOSED; + } + /** *

Called when the connection terminates. Notifies the {@link StoredClientChannel} object that we can attempt to * resume this channel in the future and stops generating messages for the server.

@@ -327,10 +349,11 @@ public class PaymentChannelClient { lock.lock(); try { checkState(connectionOpen); + step = InitStep.WAITING_FOR_CHANNEL_CLOSE; + log.info("Sending a CLOSE message to the server and waiting for response indicating successful propagation."); conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.CLOSE) .build()); - conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE); } finally { lock.unlock(); } 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 9dae4c6f..8d8060de 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 @@ -123,8 +123,12 @@ public class PaymentChannelClientState { initWalletListeners(); } - private boolean isCloseTransaction(Transaction tx) { + /** + * Returns true if the tx is a valid close transaction. + */ + public boolean isCloseTransaction(Transaction tx) { try { + tx.verify(); tx.getInput(0).verify(multisigContract.getOutput(0)); return true; } catch (VerificationException e) { diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServer.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServer.java index 67d2e183..6c0e9828 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServer.java +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServer.java @@ -3,6 +3,8 @@ package com.google.bitcoin.protocols.channels; import com.google.bitcoin.core.*; import com.google.bitcoin.protocols.channels.PaymentChannelCloseException.CloseReason; import com.google.bitcoin.utils.Threading; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.google.protobuf.ByteString; import net.jcip.annotations.GuardedBy; import org.bitcoin.paymentchannel.Protos; @@ -304,9 +306,31 @@ public class PaymentChannelServer { case CLOSE: log.info("Got CLOSE message, closing channel"); connectionClosing = true; - if (state != null) - state.close(); - conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE); + if (state != null) { + Futures.addCallback(state.close(), new FutureCallback() { + @Override + public void onSuccess(Transaction result) { + // Send the successfully accepted transaction back to the client. + final Protos.TwoWayChannelMessage.Builder msg = Protos.TwoWayChannelMessage.newBuilder(); + msg.setType(Protos.TwoWayChannelMessage.MessageType.CLOSE); + if (result != null) { + // Result can be null on various error paths, like if we never actually opened + // properly and so on. + msg.getCloseBuilder().setTx(ByteString.copyFrom(result.bitcoinSerialize())); + } + conn.sendToClient(msg.build()); + conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE); + } + + @Override + public void onFailure(Throwable t) { + log.error("Failed to broadcast close TX", t); + conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE); + } + }); + } else { + conn.destroyConnection(CloseReason.CLIENT_REQUESTED_CLOSE); + } return; case ERROR: checkState(msg.hasError()); @@ -407,9 +431,9 @@ public class PaymentChannelServer { lock.lock(); try { if (connectionOpen && !connectionClosing) { - conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder() - .setType(Protos.TwoWayChannelMessage.MessageType.CLOSE) - .build()); + final Protos.TwoWayChannelMessage.Builder msg = Protos.TwoWayChannelMessage.newBuilder(); + msg.setType(Protos.TwoWayChannelMessage.MessageType.CLOSE); + conn.sendToClient(msg.build()); conn.destroyConnection(CloseReason.SERVER_REQUESTED_CLOSE); } } finally { diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java index a1272201..ee4b89b7 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/PaymentChannelServerState.java @@ -344,7 +344,7 @@ public class PaymentChannelServerState { tx.getInput(0).setScriptSig(scriptSig); } - final SettableFuture closedFuture = SettableFuture.create(); + final SettableFuture closedFuture = SettableFuture.create(); /** *

Closes this channel and broadcasts the highest value payment transaction on the network.

* @@ -355,11 +355,12 @@ public class PaymentChannelServerState { * simply set the state to {@link State#CLOSED} and let the client handle getting its refund transaction confirmed. *

* - * @return a future which completes when the provided multisig contract successfully broadcasts, or throws if the broadcast fails for some reason - * Note that if the network simply rejects the transaction, this future will never complete, a timeout should be used. - * @throws ValueOutOfRangeException If the payment transaction would have cost more in fees to spend than it was worth + * @return a future which completes when the provided multisig contract successfully broadcasts, or throws if the + * broadcast fails for some reason. Note that if the network simply rejects the transaction, this future + * will never complete, a timeout should be used. + * @throws ValueOutOfRangeException If the payment tx would have cost more in fees to spend than it is worth. */ - public synchronized ListenableFuture close() throws ValueOutOfRangeException { + public synchronized ListenableFuture close() throws ValueOutOfRangeException { if (storedServerChannel != null) { StoredServerChannel temp = storedServerChannel; storedServerChannel = null; @@ -371,11 +372,13 @@ public class PaymentChannelServerState { } if (state.ordinal() < State.READY.ordinal()) { + log.error("Attempt to close channel in state " + state); state = State.CLOSED; - closedFuture.set(this); + closedFuture.set(null); return closedFuture; } if (state != State.READY) { + // TODO: What is this codepath for? log.warn("Failed attempt to close a channel in state " + state); return closedFuture; } @@ -386,7 +389,7 @@ public class PaymentChannelServerState { // the submission of an initial zero-valued payment during the open phase. log.warn("Closing channel that never received any payments."); state = State.CLOSED; - closedFuture.set(this); + closedFuture.set(null); return closedFuture; } Transaction tx = null; @@ -426,7 +429,7 @@ public class PaymentChannelServerState { @Override public void onSuccess(Transaction transaction) { log.info("TX {} propagated, channel successfully closed.", transaction.getHash()); state = State.CLOSED; - closedFuture.set(PaymentChannelServerState.this); + closedFuture.set(transaction); } @Override public void onFailure(Throwable throwable) { diff --git a/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelServerStates.java b/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelServerStates.java index 7199ebaf..b9985965 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelServerStates.java +++ b/core/src/main/java/com/google/bitcoin/protocols/channels/StoredPaymentChannelServerStates.java @@ -83,7 +83,6 @@ public class StoredPaymentChannelServerStates implements WalletExtension { synchronized (channel) { channel.closeConnectedHandler(); try { - //TODO add event listener to PaymentChannelServerStateManager channel.getOrCreateState(wallet, broadcaster).close(); } catch (ValueOutOfRangeException e) { e.printStackTrace(); diff --git a/core/src/main/java/org/bitcoin/paymentchannel/Protos.java b/core/src/main/java/org/bitcoin/paymentchannel/Protos.java index ef4e34fe..418eaafb 100644 --- a/core/src/main/java/org/bitcoin/paymentchannel/Protos.java +++ b/core/src/main/java/org/bitcoin/paymentchannel/Protos.java @@ -143,6 +143,20 @@ public final class Protos { */ org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder getUpdatePaymentOrBuilder(); + // optional .paymentchannels.Close close = 9; + /** + * optional .paymentchannels.Close close = 9; + */ + boolean hasClose(); + /** + * optional .paymentchannels.Close close = 9; + */ + org.bitcoin.paymentchannel.Protos.Close getClose(); + /** + * optional .paymentchannels.Close close = 9; + */ + org.bitcoin.paymentchannel.Protos.CloseOrBuilder getCloseOrBuilder(); + // optional .paymentchannels.Error error = 10; /** * optional .paymentchannels.Error error = 10; @@ -316,9 +330,22 @@ public final class Protos { bitField0_ |= 0x00000080; break; } + case 74: { + org.bitcoin.paymentchannel.Protos.Close.Builder subBuilder = null; + if (((bitField0_ & 0x00000100) == 0x00000100)) { + subBuilder = close_.toBuilder(); + } + close_ = input.readMessage(org.bitcoin.paymentchannel.Protos.Close.PARSER, extensionRegistry); + if (subBuilder != null) { + subBuilder.mergeFrom(close_); + close_ = subBuilder.buildPartial(); + } + bitField0_ |= 0x00000100; + break; + } case 82: { org.bitcoin.paymentchannel.Protos.Error.Builder subBuilder = null; - if (((bitField0_ & 0x00000100) == 0x00000100)) { + if (((bitField0_ & 0x00000200) == 0x00000200)) { subBuilder = error_.toBuilder(); } error_ = input.readMessage(org.bitcoin.paymentchannel.Protos.Error.PARSER, extensionRegistry); @@ -326,7 +353,7 @@ public final class Protos { subBuilder.mergeFrom(error_); error_ = subBuilder.buildPartial(); } - bitField0_ |= 0x00000100; + bitField0_ |= 0x00000200; break; } } @@ -419,10 +446,15 @@ public final class Protos { * CLOSE = 9; * *
-       * Note that there are no optional fields set for CLOSE, it is sent by either party to
-       * indicate that the channel is now closed and no further updates can happen. After this,
-       * the secondary takes the most recent signature it received in an UPDATE_PAYMENT and
-       * uses it to create a valid transaction, which it then broadcasts on the network.
+       * Either side can send this message. If the client sends it to the server, then the server
+       * takes the most recent signature it received in an UPDATE_PAYMENT and uses it to create a
+       * valid transaction, which it then broadcasts on the network.
+       *
+       * If the server sends it to the client, then it can either be a simple "good bye" message or
+       * it can include the fully signed close transaction and give it back so the client can
+       * eventually broadcast it as well, in case the server fails to do so for some reason (and
+       * more usefully so the client can build on it). In that case the server replies to a CLOSE
+       * with another CLOSE and then disconnects.
        * 
*/ CLOSE(8, 9), @@ -432,7 +464,8 @@ public final class Protos { *
        * Used to indicate an error condition.
        * Both parties should make an effort to send either an ERROR or a CLOSE immediately
-       * before closing the socket (unless they just received an ERROR or a CLOSE)
+       * before closing the socket (unless they just received an ERROR or a CLOSE). This is important
+       * because the protocol may not run over TCP.
        * 
*/ ERROR(9, 10), @@ -484,10 +517,15 @@ public final class Protos { * CLOSE = 9; * *
-       * Note that there are no optional fields set for CLOSE, it is sent by either party to
-       * indicate that the channel is now closed and no further updates can happen. After this,
-       * the secondary takes the most recent signature it received in an UPDATE_PAYMENT and
-       * uses it to create a valid transaction, which it then broadcasts on the network.
+       * Either side can send this message. If the client sends it to the server, then the server
+       * takes the most recent signature it received in an UPDATE_PAYMENT and uses it to create a
+       * valid transaction, which it then broadcasts on the network.
+       *
+       * If the server sends it to the client, then it can either be a simple "good bye" message or
+       * it can include the fully signed close transaction and give it back so the client can
+       * eventually broadcast it as well, in case the server fails to do so for some reason (and
+       * more usefully so the client can build on it). In that case the server replies to a CLOSE
+       * with another CLOSE and then disconnects.
        * 
*/ public static final int CLOSE_VALUE = 9; @@ -497,7 +535,8 @@ public final class Protos { *
        * Used to indicate an error condition.
        * Both parties should make an effort to send either an ERROR or a CLOSE immediately
-       * before closing the socket (unless they just received an ERROR or a CLOSE)
+       * before closing the socket (unless they just received an ERROR or a CLOSE). This is important
+       * because the protocol may not run over TCP.
        * 
*/ public static final int ERROR_VALUE = 10; @@ -763,6 +802,28 @@ public final class Protos { return updatePayment_; } + // optional .paymentchannels.Close close = 9; + public static final int CLOSE_FIELD_NUMBER = 9; + private org.bitcoin.paymentchannel.Protos.Close close_; + /** + * optional .paymentchannels.Close close = 9; + */ + public boolean hasClose() { + return ((bitField0_ & 0x00000100) == 0x00000100); + } + /** + * optional .paymentchannels.Close close = 9; + */ + public org.bitcoin.paymentchannel.Protos.Close getClose() { + return close_; + } + /** + * optional .paymentchannels.Close close = 9; + */ + public org.bitcoin.paymentchannel.Protos.CloseOrBuilder getCloseOrBuilder() { + return close_; + } + // optional .paymentchannels.Error error = 10; public static final int ERROR_FIELD_NUMBER = 10; private org.bitcoin.paymentchannel.Protos.Error error_; @@ -770,7 +831,7 @@ public final class Protos { * optional .paymentchannels.Error error = 10; */ public boolean hasError() { - return ((bitField0_ & 0x00000100) == 0x00000100); + return ((bitField0_ & 0x00000200) == 0x00000200); } /** * optional .paymentchannels.Error error = 10; @@ -794,6 +855,7 @@ public final class Protos { returnRefund_ = org.bitcoin.paymentchannel.Protos.ReturnRefund.getDefaultInstance(); provideContract_ = org.bitcoin.paymentchannel.Protos.ProvideContract.getDefaultInstance(); updatePayment_ = org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance(); + close_ = org.bitcoin.paymentchannel.Protos.Close.getDefaultInstance(); error_ = org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance(); } private byte memoizedIsInitialized = -1; @@ -847,6 +909,12 @@ public final class Protos { return false; } } + if (hasClose()) { + if (!getClose().isInitialized()) { + memoizedIsInitialized = 0; + return false; + } + } memoizedIsInitialized = 1; return true; } @@ -879,6 +947,9 @@ public final class Protos { output.writeMessage(8, updatePayment_); } if (((bitField0_ & 0x00000100) == 0x00000100)) { + output.writeMessage(9, close_); + } + if (((bitField0_ & 0x00000200) == 0x00000200)) { output.writeMessage(10, error_); } getUnknownFields().writeTo(output); @@ -923,6 +994,10 @@ public final class Protos { .computeMessageSize(8, updatePayment_); } if (((bitField0_ & 0x00000100) == 0x00000100)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(9, close_); + } + if (((bitField0_ & 0x00000200) == 0x00000200)) { size += com.google.protobuf.CodedOutputStream .computeMessageSize(10, error_); } @@ -1047,6 +1122,7 @@ public final class Protos { getReturnRefundFieldBuilder(); getProvideContractFieldBuilder(); getUpdatePaymentFieldBuilder(); + getCloseFieldBuilder(); getErrorFieldBuilder(); } } @@ -1100,12 +1176,18 @@ public final class Protos { updatePaymentBuilder_.clear(); } bitField0_ = (bitField0_ & ~0x00000080); + if (closeBuilder_ == null) { + close_ = org.bitcoin.paymentchannel.Protos.Close.getDefaultInstance(); + } else { + closeBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000100); if (errorBuilder_ == null) { error_ = org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance(); } else { errorBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000100); + bitField0_ = (bitField0_ & ~0x00000200); return this; } @@ -1197,6 +1279,14 @@ public final class Protos { if (((from_bitField0_ & 0x00000100) == 0x00000100)) { to_bitField0_ |= 0x00000100; } + if (closeBuilder_ == null) { + result.close_ = close_; + } else { + result.close_ = closeBuilder_.build(); + } + if (((from_bitField0_ & 0x00000200) == 0x00000200)) { + to_bitField0_ |= 0x00000200; + } if (errorBuilder_ == null) { result.error_ = error_; } else { @@ -1242,6 +1332,9 @@ public final class Protos { if (other.hasUpdatePayment()) { mergeUpdatePayment(other.getUpdatePayment()); } + if (other.hasClose()) { + mergeClose(other.getClose()); + } if (other.hasError()) { mergeError(other.getError()); } @@ -1296,6 +1389,12 @@ public final class Protos { return false; } } + if (hasClose()) { + if (!getClose().isInitialized()) { + + return false; + } + } return true; } @@ -2233,6 +2332,123 @@ public final class Protos { return updatePaymentBuilder_; } + // optional .paymentchannels.Close close = 9; + private org.bitcoin.paymentchannel.Protos.Close close_ = org.bitcoin.paymentchannel.Protos.Close.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.Close, org.bitcoin.paymentchannel.Protos.Close.Builder, org.bitcoin.paymentchannel.Protos.CloseOrBuilder> closeBuilder_; + /** + * optional .paymentchannels.Close close = 9; + */ + public boolean hasClose() { + return ((bitField0_ & 0x00000100) == 0x00000100); + } + /** + * optional .paymentchannels.Close close = 9; + */ + public org.bitcoin.paymentchannel.Protos.Close getClose() { + if (closeBuilder_ == null) { + return close_; + } else { + return closeBuilder_.getMessage(); + } + } + /** + * optional .paymentchannels.Close close = 9; + */ + public Builder setClose(org.bitcoin.paymentchannel.Protos.Close value) { + if (closeBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + close_ = value; + onChanged(); + } else { + closeBuilder_.setMessage(value); + } + bitField0_ |= 0x00000100; + return this; + } + /** + * optional .paymentchannels.Close close = 9; + */ + public Builder setClose( + org.bitcoin.paymentchannel.Protos.Close.Builder builderForValue) { + if (closeBuilder_ == null) { + close_ = builderForValue.build(); + onChanged(); + } else { + closeBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000100; + return this; + } + /** + * optional .paymentchannels.Close close = 9; + */ + public Builder mergeClose(org.bitcoin.paymentchannel.Protos.Close value) { + if (closeBuilder_ == null) { + if (((bitField0_ & 0x00000100) == 0x00000100) && + close_ != org.bitcoin.paymentchannel.Protos.Close.getDefaultInstance()) { + close_ = + org.bitcoin.paymentchannel.Protos.Close.newBuilder(close_).mergeFrom(value).buildPartial(); + } else { + close_ = value; + } + onChanged(); + } else { + closeBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000100; + return this; + } + /** + * optional .paymentchannels.Close close = 9; + */ + public Builder clearClose() { + if (closeBuilder_ == null) { + close_ = org.bitcoin.paymentchannel.Protos.Close.getDefaultInstance(); + onChanged(); + } else { + closeBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000100); + return this; + } + /** + * optional .paymentchannels.Close close = 9; + */ + public org.bitcoin.paymentchannel.Protos.Close.Builder getCloseBuilder() { + bitField0_ |= 0x00000100; + onChanged(); + return getCloseFieldBuilder().getBuilder(); + } + /** + * optional .paymentchannels.Close close = 9; + */ + public org.bitcoin.paymentchannel.Protos.CloseOrBuilder getCloseOrBuilder() { + if (closeBuilder_ != null) { + return closeBuilder_.getMessageOrBuilder(); + } else { + return close_; + } + } + /** + * optional .paymentchannels.Close close = 9; + */ + private com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.Close, org.bitcoin.paymentchannel.Protos.Close.Builder, org.bitcoin.paymentchannel.Protos.CloseOrBuilder> + getCloseFieldBuilder() { + if (closeBuilder_ == null) { + closeBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.bitcoin.paymentchannel.Protos.Close, org.bitcoin.paymentchannel.Protos.Close.Builder, org.bitcoin.paymentchannel.Protos.CloseOrBuilder>( + close_, + getParentForChildren(), + isClean()); + close_ = null; + } + return closeBuilder_; + } + // optional .paymentchannels.Error error = 10; private org.bitcoin.paymentchannel.Protos.Error error_ = org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance(); private com.google.protobuf.SingleFieldBuilder< @@ -2241,7 +2457,7 @@ public final class Protos { * optional .paymentchannels.Error error = 10; */ public boolean hasError() { - return ((bitField0_ & 0x00000100) == 0x00000100); + return ((bitField0_ & 0x00000200) == 0x00000200); } /** * optional .paymentchannels.Error error = 10; @@ -2266,7 +2482,7 @@ public final class Protos { } else { errorBuilder_.setMessage(value); } - bitField0_ |= 0x00000100; + bitField0_ |= 0x00000200; return this; } /** @@ -2280,7 +2496,7 @@ public final class Protos { } else { errorBuilder_.setMessage(builderForValue.build()); } - bitField0_ |= 0x00000100; + bitField0_ |= 0x00000200; return this; } /** @@ -2288,7 +2504,7 @@ public final class Protos { */ public Builder mergeError(org.bitcoin.paymentchannel.Protos.Error value) { if (errorBuilder_ == null) { - if (((bitField0_ & 0x00000100) == 0x00000100) && + if (((bitField0_ & 0x00000200) == 0x00000200) && error_ != org.bitcoin.paymentchannel.Protos.Error.getDefaultInstance()) { error_ = org.bitcoin.paymentchannel.Protos.Error.newBuilder(error_).mergeFrom(value).buildPartial(); @@ -2299,7 +2515,7 @@ public final class Protos { } else { errorBuilder_.mergeFrom(value); } - bitField0_ |= 0x00000100; + bitField0_ |= 0x00000200; return this; } /** @@ -2312,14 +2528,14 @@ public final class Protos { } else { errorBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000100); + bitField0_ = (bitField0_ & ~0x00000200); return this; } /** * optional .paymentchannels.Error error = 10; */ public org.bitcoin.paymentchannel.Protos.Error.Builder getErrorBuilder() { - bitField0_ |= 0x00000100; + bitField0_ |= 0x00000200; onChanged(); return getErrorFieldBuilder().getBuilder(); } @@ -6447,6 +6663,453 @@ public final class Protos { // @@protoc_insertion_point(class_scope:paymentchannels.UpdatePayment) } + public interface CloseOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required bytes tx = 3; + /** + * required bytes tx = 3; + * + *
+     * A copy of the fully signed final contract that closes the channel. The client can verify
+     * the transaction is correct and then commit it to their wallet.
+     * 
+ */ + boolean hasTx(); + /** + * required bytes tx = 3; + * + *
+     * A copy of the fully signed final contract that closes the channel. The client can verify
+     * the transaction is correct and then commit it to their wallet.
+     * 
+ */ + com.google.protobuf.ByteString getTx(); + } + /** + * Protobuf type {@code paymentchannels.Close} + */ + public static final class Close extends + com.google.protobuf.GeneratedMessage + implements CloseOrBuilder { + // Use Close.newBuilder() to construct. + private Close(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + this.unknownFields = builder.getUnknownFields(); + } + private Close(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); } + + private static final Close defaultInstance; + public static Close getDefaultInstance() { + return defaultInstance; + } + + public Close getDefaultInstanceForType() { + return defaultInstance; + } + + private final com.google.protobuf.UnknownFieldSet unknownFields; + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private Close( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + initFields(); + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + done = true; + } + break; + } + case 26: { + bitField0_ |= 0x00000001; + tx_ = input.readBytes(); + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e.getMessage()).setUnfinishedMessage(this); + } finally { + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Close_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Close_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.bitcoin.paymentchannel.Protos.Close.class, org.bitcoin.paymentchannel.Protos.Close.Builder.class); + } + + public static com.google.protobuf.Parser PARSER = + new com.google.protobuf.AbstractParser() { + public Close parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new Close(input, extensionRegistry); + } + }; + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + private int bitField0_; + // required bytes tx = 3; + public static final int TX_FIELD_NUMBER = 3; + private com.google.protobuf.ByteString tx_; + /** + * required bytes tx = 3; + * + *
+     * A copy of the fully signed final contract that closes the channel. The client can verify
+     * the transaction is correct and then commit it to their wallet.
+     * 
+ */ + public boolean hasTx() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * required bytes tx = 3; + * + *
+     * A copy of the fully signed final contract that closes the channel. The client can verify
+     * the transaction is correct and then commit it to their wallet.
+     * 
+ */ + public com.google.protobuf.ByteString getTx() { + return tx_; + } + + private void initFields() { + tx_ = com.google.protobuf.ByteString.EMPTY; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasTx()) { + memoizedIsInitialized = 0; + return false; + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeBytes(3, tx_); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(3, tx_); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.bitcoin.paymentchannel.Protos.Close parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.bitcoin.paymentchannel.Protos.Close parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.bitcoin.paymentchannel.Protos.Close parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.bitcoin.paymentchannel.Protos.Close parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.bitcoin.paymentchannel.Protos.Close parseFrom(java.io.InputStream input) + throws java.io.IOException { + return PARSER.parseFrom(input); + } + public static org.bitcoin.paymentchannel.Protos.Close parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseFrom(input, extensionRegistry); + } + public static org.bitcoin.paymentchannel.Protos.Close parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return PARSER.parseDelimitedFrom(input); + } + public static org.bitcoin.paymentchannel.Protos.Close parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseDelimitedFrom(input, extensionRegistry); + } + public static org.bitcoin.paymentchannel.Protos.Close parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return PARSER.parseFrom(input); + } + public static org.bitcoin.paymentchannel.Protos.Close parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseFrom(input, extensionRegistry); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.bitcoin.paymentchannel.Protos.Close prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code paymentchannels.Close} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.bitcoin.paymentchannel.Protos.CloseOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Close_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Close_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.bitcoin.paymentchannel.Protos.Close.class, org.bitcoin.paymentchannel.Protos.Close.Builder.class); + } + + // Construct using org.bitcoin.paymentchannel.Protos.Close.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + tx_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000001); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.bitcoin.paymentchannel.Protos.internal_static_paymentchannels_Close_descriptor; + } + + public org.bitcoin.paymentchannel.Protos.Close getDefaultInstanceForType() { + return org.bitcoin.paymentchannel.Protos.Close.getDefaultInstance(); + } + + public org.bitcoin.paymentchannel.Protos.Close build() { + org.bitcoin.paymentchannel.Protos.Close result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + public org.bitcoin.paymentchannel.Protos.Close buildPartial() { + org.bitcoin.paymentchannel.Protos.Close result = new org.bitcoin.paymentchannel.Protos.Close(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.tx_ = tx_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.bitcoin.paymentchannel.Protos.Close) { + return mergeFrom((org.bitcoin.paymentchannel.Protos.Close)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.bitcoin.paymentchannel.Protos.Close other) { + if (other == org.bitcoin.paymentchannel.Protos.Close.getDefaultInstance()) return this; + if (other.hasTx()) { + setTx(other.getTx()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasTx()) { + + return false; + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + org.bitcoin.paymentchannel.Protos.Close parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (org.bitcoin.paymentchannel.Protos.Close) e.getUnfinishedMessage(); + throw e; + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + private int bitField0_; + + // required bytes tx = 3; + private com.google.protobuf.ByteString tx_ = com.google.protobuf.ByteString.EMPTY; + /** + * required bytes tx = 3; + * + *
+       * A copy of the fully signed final contract that closes the channel. The client can verify
+       * the transaction is correct and then commit it to their wallet.
+       * 
+ */ + public boolean hasTx() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * required bytes tx = 3; + * + *
+       * A copy of the fully signed final contract that closes the channel. The client can verify
+       * the transaction is correct and then commit it to their wallet.
+       * 
+ */ + public com.google.protobuf.ByteString getTx() { + return tx_; + } + /** + * required bytes tx = 3; + * + *
+       * A copy of the fully signed final contract that closes the channel. The client can verify
+       * the transaction is correct and then commit it to their wallet.
+       * 
+ */ + public Builder setTx(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000001; + tx_ = value; + onChanged(); + return this; + } + /** + * required bytes tx = 3; + * + *
+       * A copy of the fully signed final contract that closes the channel. The client can verify
+       * the transaction is correct and then commit it to their wallet.
+       * 
+ */ + public Builder clearTx() { + bitField0_ = (bitField0_ & ~0x00000001); + tx_ = getDefaultInstance().getTx(); + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:paymentchannels.Close) + } + + static { + defaultInstance = new Close(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:paymentchannels.Close) + } + public interface ErrorOrBuilder extends com.google.protobuf.MessageOrBuilder { @@ -7285,6 +7948,11 @@ public final class Protos { private static com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_paymentchannels_UpdatePayment_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_paymentchannels_Close_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_paymentchannels_Close_fieldAccessorTable; private static com.google.protobuf.Descriptors.Descriptor internal_static_paymentchannels_Error_descriptor; private static @@ -7300,7 +7968,7 @@ public final class Protos { static { java.lang.String[] descriptorData = { "\n\024paymentchannel.proto\022\017paymentchannels\"" + - "\274\005\n\024TwoWayChannelMessage\022?\n\004type\030\001 \002(\01621" + + "\343\005\n\024TwoWayChannelMessage\022?\n\004type\030\001 \002(\01621" + ".paymentchannels.TwoWayChannelMessage.Me" + "ssageType\0226\n\016client_version\030\002 \001(\0132\036.paym" + "entchannels.ClientVersion\0226\n\016server_vers" + @@ -7311,31 +7979,32 @@ public final class Protos { "\006 \001(\0132\035.paymentchannels.ReturnRefund\022:\n\020", "provide_contract\030\007 \001(\0132 .paymentchannels" + ".ProvideContract\0226\n\016update_payment\030\010 \001(\013" + - "2\036.paymentchannels.UpdatePayment\022%\n\005erro" + - "r\030\n \001(\0132\026.paymentchannels.Error\"\274\001\n\013Mess" + - "ageType\022\022\n\016CLIENT_VERSION\020\001\022\022\n\016SERVER_VE" + - "RSION\020\002\022\014\n\010INITIATE\020\003\022\022\n\016PROVIDE_REFUND\020" + - "\004\022\021\n\rRETURN_REFUND\020\005\022\024\n\020PROVIDE_CONTRACT" + - "\020\006\022\020\n\014CHANNEL_OPEN\020\007\022\022\n\016UPDATE_PAYMENT\020\010" + - "\022\t\n\005CLOSE\020\t\022\t\n\005ERROR\020\n\"X\n\rClientVersion\022" + - "\r\n\005major\030\001 \002(\005\022\020\n\005minor\030\002 \001(\005:\0010\022&\n\036prev", - "ious_channel_contract_hash\030\003 \001(\014\"0\n\rServ" + - "erVersion\022\r\n\005major\030\001 \002(\005\022\020\n\005minor\030\002 \001(\005:" + - "\0010\"]\n\010Initiate\022\024\n\014multisig_key\030\001 \002(\014\022!\n\031" + - "min_accepted_channel_size\030\002 \002(\004\022\030\n\020expir" + - "e_time_secs\030\003 \002(\004\"1\n\rProvideRefund\022\024\n\014mu" + - "ltisig_key\030\001 \002(\014\022\n\n\002tx\030\002 \002(\014\"!\n\014ReturnRe" + - "fund\022\021\n\tsignature\030\001 \002(\014\"\035\n\017ProvideContra" + - "ct\022\n\n\002tx\030\001 \002(\014\"?\n\rUpdatePayment\022\033\n\023clien" + - "t_change_value\030\001 \002(\004\022\021\n\tsignature\030\002 \002(\014\"" + - "\363\001\n\005Error\0225\n\004code\030\001 \001(\0162 .paymentchannel", - "s.Error.ErrorCode:\005OTHER\022\023\n\013explanation\030" + - "\002 \001(\t\"\235\001\n\tErrorCode\022\013\n\007TIMEOUT\020\001\022\020\n\014SYNT" + - "AX_ERROR\020\002\022\031\n\025NO_ACCEPTABLE_VERSION\020\003\022\023\n" + - "\017BAD_TRANSACTION\020\004\022\031\n\025TIME_WINDOW_TOO_LA" + - "RGE\020\005\022\033\n\027CHANNEL_VALUE_TOO_LARGE\020\006\022\t\n\005OT" + - "HER\020\007B$\n\032org.bitcoin.paymentchannelB\006Pro" + - "tos" + "2\036.paymentchannels.UpdatePayment\022%\n\005clos" + + "e\030\t \001(\0132\026.paymentchannels.Close\022%\n\005error" + + "\030\n \001(\0132\026.paymentchannels.Error\"\274\001\n\013Messa" + + "geType\022\022\n\016CLIENT_VERSION\020\001\022\022\n\016SERVER_VER" + + "SION\020\002\022\014\n\010INITIATE\020\003\022\022\n\016PROVIDE_REFUND\020\004" + + "\022\021\n\rRETURN_REFUND\020\005\022\024\n\020PROVIDE_CONTRACT\020" + + "\006\022\020\n\014CHANNEL_OPEN\020\007\022\022\n\016UPDATE_PAYMENT\020\010\022" + + "\t\n\005CLOSE\020\t\022\t\n\005ERROR\020\n\"X\n\rClientVersion\022\r", + "\n\005major\030\001 \002(\005\022\020\n\005minor\030\002 \001(\005:\0010\022&\n\036previ" + + "ous_channel_contract_hash\030\003 \001(\014\"0\n\rServe" + + "rVersion\022\r\n\005major\030\001 \002(\005\022\020\n\005minor\030\002 \001(\005:\001" + + "0\"]\n\010Initiate\022\024\n\014multisig_key\030\001 \002(\014\022!\n\031m" + + "in_accepted_channel_size\030\002 \002(\004\022\030\n\020expire" + + "_time_secs\030\003 \002(\004\"1\n\rProvideRefund\022\024\n\014mul" + + "tisig_key\030\001 \002(\014\022\n\n\002tx\030\002 \002(\014\"!\n\014ReturnRef" + + "und\022\021\n\tsignature\030\001 \002(\014\"\035\n\017ProvideContrac" + + "t\022\n\n\002tx\030\001 \002(\014\"?\n\rUpdatePayment\022\033\n\023client" + + "_change_value\030\001 \002(\004\022\021\n\tsignature\030\002 \002(\014\"\023", + "\n\005Close\022\n\n\002tx\030\003 \002(\014\"\363\001\n\005Error\0225\n\004code\030\001 " + + "\001(\0162 .paymentchannels.Error.ErrorCode:\005O" + + "THER\022\023\n\013explanation\030\002 \001(\t\"\235\001\n\tErrorCode\022" + + "\013\n\007TIMEOUT\020\001\022\020\n\014SYNTAX_ERROR\020\002\022\031\n\025NO_ACC" + + "EPTABLE_VERSION\020\003\022\023\n\017BAD_TRANSACTION\020\004\022\031" + + "\n\025TIME_WINDOW_TOO_LARGE\020\005\022\033\n\027CHANNEL_VAL" + + "UE_TOO_LARGE\020\006\022\t\n\005OTHER\020\007B$\n\032org.bitcoin" + + ".paymentchannelB\006Protos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -7347,7 +8016,7 @@ public final class Protos { internal_static_paymentchannels_TwoWayChannelMessage_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_paymentchannels_TwoWayChannelMessage_descriptor, - new java.lang.String[] { "Type", "ClientVersion", "ServerVersion", "Initiate", "ProvideRefund", "ReturnRefund", "ProvideContract", "UpdatePayment", "Error", }); + new java.lang.String[] { "Type", "ClientVersion", "ServerVersion", "Initiate", "ProvideRefund", "ReturnRefund", "ProvideContract", "UpdatePayment", "Close", "Error", }); internal_static_paymentchannels_ClientVersion_descriptor = getDescriptor().getMessageTypes().get(1); internal_static_paymentchannels_ClientVersion_fieldAccessorTable = new @@ -7390,8 +8059,14 @@ public final class Protos { com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_paymentchannels_UpdatePayment_descriptor, new java.lang.String[] { "ClientChangeValue", "Signature", }); - internal_static_paymentchannels_Error_descriptor = + internal_static_paymentchannels_Close_descriptor = getDescriptor().getMessageTypes().get(8); + internal_static_paymentchannels_Close_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_paymentchannels_Close_descriptor, + new java.lang.String[] { "Tx", }); + internal_static_paymentchannels_Error_descriptor = + getDescriptor().getMessageTypes().get(9); internal_static_paymentchannels_Error_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_paymentchannels_Error_descriptor, diff --git a/core/src/paymentchannel.proto b/core/src/paymentchannel.proto index 9dc621b7..12c4bb66 100644 --- a/core/src/paymentchannel.proto +++ b/core/src/paymentchannel.proto @@ -47,15 +47,21 @@ message TwoWayChannelMessage { // message. CHANNEL_OPEN = 7; UPDATE_PAYMENT = 8; - // Note that there are no optional fields set for CLOSE, it is sent by either party to - // indicate that the channel is now closed and no further updates can happen. After this, - // the secondary takes the most recent signature it received in an UPDATE_PAYMENT and - // uses it to create a valid transaction, which it then broadcasts on the network. + // Either side can send this message. If the client sends it to the server, then the server + // takes the most recent signature it received in an UPDATE_PAYMENT and uses it to create a + // valid transaction, which it then broadcasts on the network. + // + // If the server sends it to the client, then it can either be a simple "good bye" message or + // it can include the fully signed close transaction and give it back so the client can + // eventually broadcast it as well, in case the server fails to do so for some reason (and + // more usefully so the client can build on it). In that case the server replies to a CLOSE + // with another CLOSE and then disconnects. CLOSE = 9; // Used to indicate an error condition. // Both parties should make an effort to send either an ERROR or a CLOSE immediately - // before closing the socket (unless they just received an ERROR or a CLOSE) + // before closing the socket (unless they just received an ERROR or a CLOSE). This is important + // because the protocol may not run over TCP. ERROR = 10; }; @@ -72,6 +78,7 @@ message TwoWayChannelMessage { optional ReturnRefund return_refund = 6; optional ProvideContract provide_contract = 7; optional UpdatePayment update_payment = 8; + optional Close close = 9; optional Error error = 10; } @@ -185,6 +192,11 @@ message UpdatePayment { required bytes signature = 2; } +message Close { + // A copy of the fully signed final contract that closes the channel. The client can verify + // the transaction is correct and then commit it to their wallet. + required bytes tx = 3; +} // An Error can be sent by either party at any time // Both parties should make an effort to send either an ERROR or a CLOSE immediately before 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 ddaac751..588b84c8 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 @@ -644,13 +644,23 @@ public class ChannelConnectionTest extends TestWithWallet { client.incrementPayment(Utils.CENT); client.incrementPayment(Utils.CENT); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT)); + pair.serverRecorder.q.take(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT)); + pair.serverRecorder.q.take(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.UPDATE_PAYMENT)); + pair.serverRecorder.q.take(); + // 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(); + // Server sends back the close TX it just broadcast. + final Protos.TwoWayChannelMessage closeMsg = pair.serverRecorder.checkNextMsg(MessageType.CLOSE); + final Transaction closeTx = new Transaction(params, closeMsg.getClose().getTx().toByteArray()); + assertEquals(close, closeTx); + client.receiveMessage(closeMsg); + assertNotNull(wallet.getTransaction(closeTx.getHash())); // Close TX entered the wallet. sendMoneyToWallet(close, AbstractBlockChain.NewBlockType.BEST_CHAIN); client.connectionClosed(); server.connectionClosed(); diff --git a/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java b/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java index f8fae96f..ff3e1cc1 100644 --- a/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java +++ b/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java @@ -20,7 +20,6 @@ import com.google.bitcoin.core.*; import com.google.bitcoin.script.Script; import com.google.bitcoin.script.ScriptBuilder; import com.google.bitcoin.utils.TestWithWallet; -import com.google.bitcoin.wallet.DefaultCoinSelector; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; @@ -189,29 +188,19 @@ public class PaymentChannelStateTest extends TestWithWallet { final TxFuturePair pair2 = broadcasts.take(); Transaction closeTx = pair2.tx; pair2.future.set(closeTx); + final Transaction reserializedCloseTx = new Transaction(params, closeTx.bitcoinSerialize()); assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState()); + // ... and on the client side. + wallet.receivePending(reserializedCloseTx, null); + assertEquals(PaymentChannelClientState.State.CLOSED, clientState.getState()); // Create a block with the payment transaction in it and give it to both wallets - chain.add(makeSolvedTestBlock(blockStore.getChainHead().getHeader(), new Transaction(params, closeTx.bitcoinSerialize()))); + chain.add(makeSolvedTestBlock(blockStore.getChainHead().getHeader(), reserializedCloseTx)); - assertEquals(size.multiply(BigInteger.valueOf(5)), serverWallet.getBalance(new DefaultCoinSelector() { - @Override - protected boolean shouldSelect(Transaction tx) { - if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) - return true; - return false; - } - })); + assertEquals(size.multiply(BigInteger.valueOf(5)), serverWallet.getBalance()); assertEquals(0, serverWallet.getPendingTransactions().size()); - assertEquals(Utils.COIN.subtract(size.multiply(BigInteger.valueOf(5))), wallet.getBalance(new DefaultCoinSelector() { - @Override - protected boolean shouldSelect(Transaction tx) { - if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) - return true; - return false; - } - })); + assertEquals(Utils.COIN.subtract(size.multiply(BigInteger.valueOf(5))), wallet.getBalance()); assertEquals(0, wallet.getPendingTransactions().size()); assertEquals(3, wallet.getTransactions(false).size());