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