diff --git a/core/src/main/java/org/bitcoin/paymentchannel/Protos.java b/core/src/main/java/org/bitcoin/paymentchannel/Protos.java index df09d380e..22dfa808c 100644 --- a/core/src/main/java/org/bitcoin/paymentchannel/Protos.java +++ b/core/src/main/java/org/bitcoin/paymentchannel/Protos.java @@ -6006,11 +6006,17 @@ public final class Protos { * *
* The serialized bytes of the transaction in Satoshi format. + * For version 1: * * It must be signed and completely valid and ready for broadcast (ie it includes the * necessary fees) TODO: tell the client how much fee it needs * * Its first output must be a 2-of-2 multisig output with the first pubkey being the * primary's and the second being the secondary's (ie the script must be exactly "OP_2 * ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG") + * For version 2: + * * It must be signed and completely valid and ready for broadcast (ie it includes the + * necessary fees) TODO: tell the client how much fee it needs + * * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the + * primary's and the second being the secondary's. **/ boolean hasTx(); @@ -6019,11 +6025,17 @@ public final class Protos { * *
* The serialized bytes of the transaction in Satoshi format. + * For version 1: * * It must be signed and completely valid and ready for broadcast (ie it includes the * necessary fees) TODO: tell the client how much fee it needs * * Its first output must be a 2-of-2 multisig output with the first pubkey being the * primary's and the second being the secondary's (ie the script must be exactly "OP_2 * ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG") + * For version 2: + * * It must be signed and completely valid and ready for broadcast (ie it includes the + * necessary fees) TODO: tell the client how much fee it needs + * * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the + * primary's and the second being the secondary's. **/ com.google.protobuf.ByteString getTx(); @@ -6061,6 +6073,29 @@ public final class Protos { * */ org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder getInitialPaymentOrBuilder(); + + /** + *
optional bytes client_key = 3;
+ *
+ * + * This field is added in protocol version 2 to send the client public key to the server. + * In version 1 it isn't used. + * This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms + * are accepted. It is only used in the creation of the multisig contract. + *+ */ + boolean hasClientKey(); + /** + *
optional bytes client_key = 3;
+ *
+ * + * This field is added in protocol version 2 to send the client public key to the server. + * In version 1 it isn't used. + * This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms + * are accepted. It is only used in the creation of the multisig contract. + *+ */ + com.google.protobuf.ByteString getClientKey(); } /** * Protobuf type {@code paymentchannels.ProvideContract} @@ -6136,6 +6171,11 @@ public final class Protos { bitField0_ |= 0x00000002; break; } + case 26: { + bitField0_ |= 0x00000004; + clientKey_ = input.readBytes(); + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -6183,11 +6223,17 @@ public final class Protos { * *
* The serialized bytes of the transaction in Satoshi format. + * For version 1: * * It must be signed and completely valid and ready for broadcast (ie it includes the * necessary fees) TODO: tell the client how much fee it needs * * Its first output must be a 2-of-2 multisig output with the first pubkey being the * primary's and the second being the secondary's (ie the script must be exactly "OP_2 * ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG") + * For version 2: + * * It must be signed and completely valid and ready for broadcast (ie it includes the + * necessary fees) TODO: tell the client how much fee it needs + * * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the + * primary's and the second being the secondary's. **/ public boolean hasTx() { @@ -6198,11 +6244,17 @@ public final class Protos { * *
* The serialized bytes of the transaction in Satoshi format. + * For version 1: * * It must be signed and completely valid and ready for broadcast (ie it includes the * necessary fees) TODO: tell the client how much fee it needs * * Its first output must be a 2-of-2 multisig output with the first pubkey being the * primary's and the second being the secondary's (ie the script must be exactly "OP_2 * ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG") + * For version 2: + * * It must be signed and completely valid and ready for broadcast (ie it includes the + * necessary fees) TODO: tell the client how much fee it needs + * * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the + * primary's and the second being the secondary's. **/ public com.google.protobuf.ByteString getTx() { @@ -6251,9 +6303,39 @@ public final class Protos { return initialPayment_; } + public static final int CLIENT_KEY_FIELD_NUMBER = 3; + private com.google.protobuf.ByteString clientKey_; + /** + *
optional bytes client_key = 3;
+ *
+ * + * This field is added in protocol version 2 to send the client public key to the server. + * In version 1 it isn't used. + * This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms + * are accepted. It is only used in the creation of the multisig contract. + *+ */ + public boolean hasClientKey() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + /** + *
optional bytes client_key = 3;
+ *
+ * + * This field is added in protocol version 2 to send the client public key to the server. + * In version 1 it isn't used. + * This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms + * are accepted. It is only used in the creation of the multisig contract. + *+ */ + public com.google.protobuf.ByteString getClientKey() { + return clientKey_; + } + private void initFields() { tx_ = com.google.protobuf.ByteString.EMPTY; initialPayment_ = org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance(); + clientKey_ = com.google.protobuf.ByteString.EMPTY; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -6286,6 +6368,9 @@ public final class Protos { if (((bitField0_ & 0x00000002) == 0x00000002)) { output.writeMessage(2, initialPayment_); } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + output.writeBytes(3, clientKey_); + } getUnknownFields().writeTo(output); } @@ -6303,6 +6388,10 @@ public final class Protos { size += com.google.protobuf.CodedOutputStream .computeMessageSize(2, initialPayment_); } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(3, clientKey_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -6433,6 +6522,8 @@ public final class Protos { initialPaymentBuilder_.clear(); } bitField0_ = (bitField0_ & ~0x00000002); + clientKey_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000004); return this; } @@ -6473,6 +6564,10 @@ public final class Protos { } else { result.initialPayment_ = initialPaymentBuilder_.build(); } + if (((from_bitField0_ & 0x00000004) == 0x00000004)) { + to_bitField0_ |= 0x00000004; + } + result.clientKey_ = clientKey_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -6495,6 +6590,9 @@ public final class Protos { if (other.hasInitialPayment()) { mergeInitialPayment(other.getInitialPayment()); } + if (other.hasClientKey()) { + setClientKey(other.getClientKey()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -6540,11 +6638,17 @@ public final class Protos { * *
* The serialized bytes of the transaction in Satoshi format. + * For version 1: * * It must be signed and completely valid and ready for broadcast (ie it includes the * necessary fees) TODO: tell the client how much fee it needs * * Its first output must be a 2-of-2 multisig output with the first pubkey being the * primary's and the second being the secondary's (ie the script must be exactly "OP_2 * ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG") + * For version 2: + * * It must be signed and completely valid and ready for broadcast (ie it includes the + * necessary fees) TODO: tell the client how much fee it needs + * * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the + * primary's and the second being the secondary's. **/ public boolean hasTx() { @@ -6555,11 +6659,17 @@ public final class Protos { * *
* The serialized bytes of the transaction in Satoshi format. + * For version 1: * * It must be signed and completely valid and ready for broadcast (ie it includes the * necessary fees) TODO: tell the client how much fee it needs * * Its first output must be a 2-of-2 multisig output with the first pubkey being the * primary's and the second being the secondary's (ie the script must be exactly "OP_2 * ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG") + * For version 2: + * * It must be signed and completely valid and ready for broadcast (ie it includes the + * necessary fees) TODO: tell the client how much fee it needs + * * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the + * primary's and the second being the secondary's. **/ public com.google.protobuf.ByteString getTx() { @@ -6570,11 +6680,17 @@ public final class Protos { * *
* The serialized bytes of the transaction in Satoshi format. + * For version 1: * * It must be signed and completely valid and ready for broadcast (ie it includes the * necessary fees) TODO: tell the client how much fee it needs * * Its first output must be a 2-of-2 multisig output with the first pubkey being the * primary's and the second being the secondary's (ie the script must be exactly "OP_2 * ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG") + * For version 2: + * * It must be signed and completely valid and ready for broadcast (ie it includes the + * necessary fees) TODO: tell the client how much fee it needs + * * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the + * primary's and the second being the secondary's. **/ public Builder setTx(com.google.protobuf.ByteString value) { @@ -6591,11 +6707,17 @@ public final class Protos { * *
* The serialized bytes of the transaction in Satoshi format. + * For version 1: * * It must be signed and completely valid and ready for broadcast (ie it includes the * necessary fees) TODO: tell the client how much fee it needs * * Its first output must be a 2-of-2 multisig output with the first pubkey being the * primary's and the second being the secondary's (ie the script must be exactly "OP_2 * ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG") + * For version 2: + * * It must be signed and completely valid and ready for broadcast (ie it includes the + * necessary fees) TODO: tell the client how much fee it needs + * * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the + * primary's and the second being the secondary's. **/ public Builder clearTx() { @@ -6784,6 +6906,69 @@ public final class Protos { return initialPaymentBuilder_; } + private com.google.protobuf.ByteString clientKey_ = com.google.protobuf.ByteString.EMPTY; + /** + *
optional bytes client_key = 3;
+ *
+ * + * This field is added in protocol version 2 to send the client public key to the server. + * In version 1 it isn't used. + * This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms + * are accepted. It is only used in the creation of the multisig contract. + *+ */ + public boolean hasClientKey() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + /** + *
optional bytes client_key = 3;
+ *
+ * + * This field is added in protocol version 2 to send the client public key to the server. + * In version 1 it isn't used. + * This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms + * are accepted. It is only used in the creation of the multisig contract. + *+ */ + public com.google.protobuf.ByteString getClientKey() { + return clientKey_; + } + /** + *
optional bytes client_key = 3;
+ *
+ * + * This field is added in protocol version 2 to send the client public key to the server. + * In version 1 it isn't used. + * This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms + * are accepted. It is only used in the creation of the multisig contract. + *+ */ + public Builder setClientKey(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000004; + clientKey_ = value; + onChanged(); + return this; + } + /** + *
optional bytes client_key = 3;
+ *
+ * + * This field is added in protocol version 2 to send the client public key to the server. + * In version 1 it isn't used. + * This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms + * are accepted. It is only used in the creation of the multisig contract. + *+ */ + public Builder clearClientKey() { + bitField0_ = (bitField0_ & ~0x00000004); + clientKey_ = getDefaultInstance().getClientKey(); + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:paymentchannels.ProvideContract) } @@ -9439,21 +9624,22 @@ public final class Protos { "d_channel_size\030\002 \002(\004\022\030\n\020expire_time_secs" + "\030\003 \002(\004\022\023\n\013min_payment\030\004 \002(\004\"1\n\rProvideRe" + "fund\022\024\n\014multisig_key\030\001 \002(\014\022\n\n\002tx\030\002 \002(\014\"!", - "\n\014ReturnRefund\022\021\n\tsignature\030\001 \002(\014\"V\n\017Pro" + + "\n\014ReturnRefund\022\021\n\tsignature\030\001 \002(\014\"j\n\017Pro" + "videContract\022\n\n\002tx\030\001 \002(\014\0227\n\017initial_paym" + "ent\030\002 \002(\0132\036.paymentchannels.UpdatePaymen" + - "t\"M\n\rUpdatePayment\022\033\n\023client_change_valu" + - "e\030\001 \002(\004\022\021\n\tsignature\030\002 \002(\014\022\014\n\004info\030\003 \001(\014" + - "\"\032\n\nPaymentAck\022\014\n\004info\030\001 \001(\014\"\030\n\nSettleme" + - "nt\022\n\n\002tx\030\003 \002(\014\"\251\002\n\005Error\0225\n\004code\030\001 \001(\0162 " + - ".paymentchannels.Error.ErrorCode:\005OTHER\022" + - "\023\n\013explanation\030\002 \001(\t\022\026\n\016expected_value\030\003" + - " \001(\004\"\273\001\n\tErrorCode\022\013\n\007TIMEOUT\020\001\022\020\n\014SYNTA", - "X_ERROR\020\002\022\031\n\025NO_ACCEPTABLE_VERSION\020\003\022\023\n\017" + - "BAD_TRANSACTION\020\004\022\034\n\030TIME_WINDOW_UNACCEP" + - "TABLE\020\005\022\033\n\027CHANNEL_VALUE_TOO_LARGE\020\006\022\031\n\025" + - "MIN_PAYMENT_TOO_LARGE\020\007\022\t\n\005OTHER\020\010B$\n\032or" + - "g.bitcoin.paymentchannelB\006Protos" + "t\022\022\n\nclient_key\030\003 \001(\014\"M\n\rUpdatePayment\022\033" + + "\n\023client_change_value\030\001 \002(\004\022\021\n\tsignature" + + "\030\002 \002(\014\022\014\n\004info\030\003 \001(\014\"\032\n\nPaymentAck\022\014\n\004in" + + "fo\030\001 \001(\014\"\030\n\nSettlement\022\n\n\002tx\030\003 \002(\014\"\251\002\n\005E" + + "rror\0225\n\004code\030\001 \001(\0162 .paymentchannels.Err" + + "or.ErrorCode:\005OTHER\022\023\n\013explanation\030\002 \001(\t" + + "\022\026\n\016expected_value\030\003 \001(\004\"\273\001\n\tErrorCode\022\013", + "\n\007TIMEOUT\020\001\022\020\n\014SYNTAX_ERROR\020\002\022\031\n\025NO_ACCE" + + "PTABLE_VERSION\020\003\022\023\n\017BAD_TRANSACTION\020\004\022\034\n" + + "\030TIME_WINDOW_UNACCEPTABLE\020\005\022\033\n\027CHANNEL_V" + + "ALUE_TOO_LARGE\020\006\022\031\n\025MIN_PAYMENT_TOO_LARG" + + "E\020\007\022\t\n\005OTHER\020\010B$\n\032org.bitcoin.paymentcha" + + "nnelB\006Protos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() { @@ -9508,7 +9694,7 @@ public final class Protos { internal_static_paymentchannels_ProvideContract_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_paymentchannels_ProvideContract_descriptor, - new java.lang.String[] { "Tx", "InitialPayment", }); + new java.lang.String[] { "Tx", "InitialPayment", "ClientKey", }); internal_static_paymentchannels_UpdatePayment_descriptor = getDescriptor().getMessageTypes().get(7); internal_static_paymentchannels_UpdatePayment_fieldAccessorTable = new diff --git a/core/src/main/java/org/bitcoinj/core/Wallet.java b/core/src/main/java/org/bitcoinj/core/Wallet.java index d223a7aa9..1cbbba0ab 100644 --- a/core/src/main/java/org/bitcoinj/core/Wallet.java +++ b/core/src/main/java/org/bitcoinj/core/Wallet.java @@ -3818,11 +3818,12 @@ public class Wallet extends BaseTaggableObject return toCLTVPaymentChannel(params, BigInteger.valueOf(time), from, to, value); } - public static SendRequest toCLTVPaymentChannel(NetworkParameters params, long lockTime, ECKey from, ECKey to, Coin value) { - return toCLTVPaymentChannel(params, BigInteger.valueOf(lockTime), from, to, value); + public static SendRequest toCLTVPaymentChannel(NetworkParameters params, int releaseBlock, ECKey from, ECKey to, Coin value) { + checkArgument(0 <= releaseBlock && releaseBlock < Transaction.LOCKTIME_THRESHOLD, "Block number was too large"); + return toCLTVPaymentChannel(params, BigInteger.valueOf(releaseBlock), from, to, value); } - private static SendRequest toCLTVPaymentChannel(NetworkParameters params, BigInteger time, ECKey from, ECKey to, Coin value) { + public static SendRequest toCLTVPaymentChannel(NetworkParameters params, BigInteger time, ECKey from, ECKey to, Coin value) { SendRequest req = new SendRequest(); Script output = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to); req.tx = new Transaction(params); @@ -4223,6 +4224,7 @@ public class Wallet extends BaseTaggableObject log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i); continue; } catch (ScriptException e) { + log.debug("Input contained an incorrect signature", e); // Expected. } diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/ClientState.java b/core/src/main/java/org/bitcoinj/protocols/channels/ClientState.java index 76079af6b..a3e0db62c 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/ClientState.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/ClientState.java @@ -772,10 +772,18 @@ public final class ClientState { /** *
required uint64 refundFees = 6;
+ *
+ * + * Fees required to refund the transaction. + **/ boolean hasRefundFees(); /** *
required uint64 refundFees = 6;
+ *
+ * + * Fees required to refund the transaction. + **/ long getRefundFees(); @@ -799,6 +807,49 @@ public final class ClientState { * */ com.google.protobuf.ByteString getCloseTransactionHash(); + + /** + *
optional uint32 majorVersion = 9 [default = 1];
+ */
+ boolean hasMajorVersion();
+ /**
+ * optional uint32 majorVersion = 9 [default = 1];
+ */
+ int getMajorVersion();
+
+ /**
+ * optional uint64 expiryTime = 10;
+ *
+ * + * The expiry time of the CLTV lock. Only used in protocol v2. + *+ */ + boolean hasExpiryTime(); + /** + *
optional uint64 expiryTime = 10;
+ *
+ * + * The expiry time of the CLTV lock. Only used in protocol v2. + *+ */ + long getExpiryTime(); + + /** + *
optional bytes serverKey = 11;
+ *
+ * + * The server's public key. Only used in protocol v2. + *+ */ + boolean hasServerKey(); + /** + *
optional bytes serverKey = 11;
+ *
+ * + * The server's public key. Only used in protocol v2. + *+ */ + com.google.protobuf.ByteString getServerKey(); } /** * Protobuf type {@code paymentchannels.StoredClientPaymentChannel} @@ -897,6 +948,21 @@ public final class ClientState { myPublicKey_ = input.readBytes(); break; } + case 72: { + bitField0_ |= 0x00000100; + majorVersion_ = input.readUInt32(); + break; + } + case 80: { + bitField0_ |= 0x00000200; + expiryTime_ = input.readUInt64(); + break; + } + case 90: { + bitField0_ |= 0x00000400; + serverKey_ = input.readBytes(); + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -1039,12 +1105,20 @@ public final class ClientState { private long refundFees_; /** *
required uint64 refundFees = 6;
+ *
+ * + * Fees required to refund the transaction. + **/ public boolean hasRefundFees() { return ((bitField0_ & 0x00000040) == 0x00000040); } /** *
required uint64 refundFees = 6;
+ *
+ * + * Fees required to refund the transaction. + **/ public long getRefundFees() { return refundFees_; @@ -1077,6 +1151,67 @@ public final class ClientState { return closeTransactionHash_; } + public static final int MAJORVERSION_FIELD_NUMBER = 9; + private int majorVersion_; + /** + *
optional uint32 majorVersion = 9 [default = 1];
+ */
+ public boolean hasMajorVersion() {
+ return ((bitField0_ & 0x00000100) == 0x00000100);
+ }
+ /**
+ * optional uint32 majorVersion = 9 [default = 1];
+ */
+ public int getMajorVersion() {
+ return majorVersion_;
+ }
+
+ public static final int EXPIRYTIME_FIELD_NUMBER = 10;
+ private long expiryTime_;
+ /**
+ * optional uint64 expiryTime = 10;
+ *
+ * + * The expiry time of the CLTV lock. Only used in protocol v2. + *+ */ + public boolean hasExpiryTime() { + return ((bitField0_ & 0x00000200) == 0x00000200); + } + /** + *
optional uint64 expiryTime = 10;
+ *
+ * + * The expiry time of the CLTV lock. Only used in protocol v2. + *+ */ + public long getExpiryTime() { + return expiryTime_; + } + + public static final int SERVERKEY_FIELD_NUMBER = 11; + private com.google.protobuf.ByteString serverKey_; + /** + *
optional bytes serverKey = 11;
+ *
+ * + * The server's public key. Only used in protocol v2. + *+ */ + public boolean hasServerKey() { + return ((bitField0_ & 0x00000400) == 0x00000400); + } + /** + *
optional bytes serverKey = 11;
+ *
+ * + * The server's public key. Only used in protocol v2. + *+ */ + public com.google.protobuf.ByteString getServerKey() { + return serverKey_; + } + private void initFields() { id_ = com.google.protobuf.ByteString.EMPTY; contractTransaction_ = com.google.protobuf.ByteString.EMPTY; @@ -1086,6 +1221,9 @@ public final class ClientState { valueToMe_ = 0L; refundFees_ = 0L; closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY; + majorVersion_ = 1; + expiryTime_ = 0L; + serverKey_ = com.google.protobuf.ByteString.EMPTY; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -1152,6 +1290,15 @@ public final class ClientState { if (((bitField0_ & 0x00000008) == 0x00000008)) { output.writeBytes(8, myPublicKey_); } + if (((bitField0_ & 0x00000100) == 0x00000100)) { + output.writeUInt32(9, majorVersion_); + } + if (((bitField0_ & 0x00000200) == 0x00000200)) { + output.writeUInt64(10, expiryTime_); + } + if (((bitField0_ & 0x00000400) == 0x00000400)) { + output.writeBytes(11, serverKey_); + } getUnknownFields().writeTo(output); } @@ -1193,6 +1340,18 @@ public final class ClientState { size += com.google.protobuf.CodedOutputStream .computeBytesSize(8, myPublicKey_); } + if (((bitField0_ & 0x00000100) == 0x00000100)) { + size += com.google.protobuf.CodedOutputStream + .computeUInt32Size(9, majorVersion_); + } + if (((bitField0_ & 0x00000200) == 0x00000200)) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(10, expiryTime_); + } + if (((bitField0_ & 0x00000400) == 0x00000400)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(11, serverKey_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -1331,6 +1490,12 @@ public final class ClientState { bitField0_ = (bitField0_ & ~0x00000040); closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY; bitField0_ = (bitField0_ & ~0x00000080); + majorVersion_ = 1; + bitField0_ = (bitField0_ & ~0x00000100); + expiryTime_ = 0L; + bitField0_ = (bitField0_ & ~0x00000200); + serverKey_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000400); return this; } @@ -1391,6 +1556,18 @@ public final class ClientState { to_bitField0_ |= 0x00000080; } result.closeTransactionHash_ = closeTransactionHash_; + if (((from_bitField0_ & 0x00000100) == 0x00000100)) { + to_bitField0_ |= 0x00000100; + } + result.majorVersion_ = majorVersion_; + if (((from_bitField0_ & 0x00000200) == 0x00000200)) { + to_bitField0_ |= 0x00000200; + } + result.expiryTime_ = expiryTime_; + if (((from_bitField0_ & 0x00000400) == 0x00000400)) { + to_bitField0_ |= 0x00000400; + } + result.serverKey_ = serverKey_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -1431,6 +1608,15 @@ public final class ClientState { if (other.hasCloseTransactionHash()) { setCloseTransactionHash(other.getCloseTransactionHash()); } + if (other.hasMajorVersion()) { + setMajorVersion(other.getMajorVersion()); + } + if (other.hasExpiryTime()) { + setExpiryTime(other.getExpiryTime()); + } + if (other.hasServerKey()) { + setServerKey(other.getServerKey()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -1712,18 +1898,30 @@ public final class ClientState { private long refundFees_ ; /** *
required uint64 refundFees = 6;
+ *
+ * + * Fees required to refund the transaction. + **/ public boolean hasRefundFees() { return ((bitField0_ & 0x00000040) == 0x00000040); } /** *
required uint64 refundFees = 6;
+ *
+ * + * Fees required to refund the transaction. + **/ public long getRefundFees() { return refundFees_; } /** *
required uint64 refundFees = 6;
+ *
+ * + * Fees required to refund the transaction. + **/ public Builder setRefundFees(long value) { bitField0_ |= 0x00000040; @@ -1733,6 +1931,10 @@ public final class ClientState { } /** *
required uint64 refundFees = 6;
+ *
+ * + * Fees required to refund the transaction. + **/ public Builder clearRefundFees() { bitField0_ = (bitField0_ & ~0x00000040); @@ -1800,6 +2002,137 @@ public final class ClientState { return this; } + private int majorVersion_ = 1; + /** + *
optional uint32 majorVersion = 9 [default = 1];
+ */
+ public boolean hasMajorVersion() {
+ return ((bitField0_ & 0x00000100) == 0x00000100);
+ }
+ /**
+ * optional uint32 majorVersion = 9 [default = 1];
+ */
+ public int getMajorVersion() {
+ return majorVersion_;
+ }
+ /**
+ * optional uint32 majorVersion = 9 [default = 1];
+ */
+ public Builder setMajorVersion(int value) {
+ bitField0_ |= 0x00000100;
+ majorVersion_ = value;
+ onChanged();
+ return this;
+ }
+ /**
+ * optional uint32 majorVersion = 9 [default = 1];
+ */
+ public Builder clearMajorVersion() {
+ bitField0_ = (bitField0_ & ~0x00000100);
+ majorVersion_ = 1;
+ onChanged();
+ return this;
+ }
+
+ private long expiryTime_ ;
+ /**
+ * optional uint64 expiryTime = 10;
+ *
+ * + * The expiry time of the CLTV lock. Only used in protocol v2. + *+ */ + public boolean hasExpiryTime() { + return ((bitField0_ & 0x00000200) == 0x00000200); + } + /** + *
optional uint64 expiryTime = 10;
+ *
+ * + * The expiry time of the CLTV lock. Only used in protocol v2. + *+ */ + public long getExpiryTime() { + return expiryTime_; + } + /** + *
optional uint64 expiryTime = 10;
+ *
+ * + * The expiry time of the CLTV lock. Only used in protocol v2. + *+ */ + public Builder setExpiryTime(long value) { + bitField0_ |= 0x00000200; + expiryTime_ = value; + onChanged(); + return this; + } + /** + *
optional uint64 expiryTime = 10;
+ *
+ * + * The expiry time of the CLTV lock. Only used in protocol v2. + *+ */ + public Builder clearExpiryTime() { + bitField0_ = (bitField0_ & ~0x00000200); + expiryTime_ = 0L; + onChanged(); + return this; + } + + private com.google.protobuf.ByteString serverKey_ = com.google.protobuf.ByteString.EMPTY; + /** + *
optional bytes serverKey = 11;
+ *
+ * + * The server's public key. Only used in protocol v2. + *+ */ + public boolean hasServerKey() { + return ((bitField0_ & 0x00000400) == 0x00000400); + } + /** + *
optional bytes serverKey = 11;
+ *
+ * + * The server's public key. Only used in protocol v2. + *+ */ + public com.google.protobuf.ByteString getServerKey() { + return serverKey_; + } + /** + *
optional bytes serverKey = 11;
+ *
+ * + * The server's public key. Only used in protocol v2. + *+ */ + public Builder setServerKey(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000400; + serverKey_ = value; + onChanged(); + return this; + } + /** + *
optional bytes serverKey = 11;
+ *
+ * + * The server's public key. Only used in protocol v2. + *+ */ + public Builder clearServerKey() { + bitField0_ = (bitField0_ & ~0x00000400); + serverKey_ = getDefaultInstance().getServerKey(); + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:paymentchannels.StoredClientPaymentChannel) } @@ -1833,13 +2166,15 @@ public final class ClientState { "\n storedclientpaymentchannel.proto\022\017paym" + "entchannels\"\\\n\033StoredClientPaymentChanne" + "ls\022=\n\010channels\030\001 \003(\0132+.paymentchannels.S" + - "toredClientPaymentChannel\"\311\001\n\032StoredClie" + + "toredClientPaymentChannel\"\211\002\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\023\n\013myPublicKey\030\010 \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(\004\022" + - "\034\n\024closeTransactionHash\030\007 \001(\014B.\n\037org.bit" + - "coinj.protocols.channelsB\013ClientState" + "\034\n\024closeTransactionHash\030\007 \001(\014\022\027\n\014majorVe" + + "rsion\030\t \001(\r:\0011\022\022\n\nexpiryTime\030\n \001(\004\022\021\n\tse", + "rverKey\030\013 \001(\014B.\n\037org.bitcoinj.protocols." + + "channelsB\013ClientState" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() { @@ -1864,7 +2199,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", "MyPublicKey", "MyKey", "ValueToMe", "RefundFees", "CloseTransactionHash", }); + new java.lang.String[] { "Id", "ContractTransaction", "RefundTransaction", "MyPublicKey", "MyKey", "ValueToMe", "RefundFees", "CloseTransactionHash", "MajorVersion", "ExpiryTime", "ServerKey", }); } // @@protoc_insertion_point(outer_class_scope) diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java index beb37b13d..1b5c8c1e8 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java @@ -52,12 +52,12 @@ import static com.google.common.base.Preconditions.checkState; */ public class PaymentChannelClient implements IPaymentChannelClient { private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelClient.class); - private static final int CLIENT_MAJOR_VERSION = 1; - public final int CLIENT_MINOR_VERSION = 0; - private static final int SERVER_MAJOR_VERSION = 1; protected final ReentrantLock lock = Threading.lock("channelclient"); + // Used to track the negotiated version number + @GuardedBy("lock") private int majorVersion; + @GuardedBy("lock") private final ClientConnection conn; // Used to keep track of whether or not the "socket" ie connection is open and we can generate messages @@ -79,6 +79,42 @@ public class PaymentChannelClient implements IPaymentChannelClient { } @GuardedBy("lock") private InitStep step = InitStep.WAITING_FOR_CONNECTION_OPEN; + public enum VersionSelector { + VERSION_1, + VERSION_2_ALLOW_1, + VERSION_2; + + public int getRequestedMajorVersion() { + switch (this) { + case VERSION_1: + return 1; + case VERSION_2_ALLOW_1: + case VERSION_2: + default: + return 2; + } + } + + public int getRequestedMinorVersion() { + return 0; + } + + public boolean isServerVersionAccepted(int major, int minor) { + switch (this) { + case VERSION_1: + return major == 1; + case VERSION_2_ALLOW_1: + return major == 1 || major == 2; + case VERSION_2: + return major == 2; + default: + return false; + } + } + } + + private final VersionSelector versionSelector; + // Will either hold the StoredClientChannel of this channel or null after connectionOpen private StoredClientChannel storedChannel; // An arbitrary hash which identifies this channel (specified by the API user) @@ -129,8 +165,36 @@ public class PaymentChannelClient implements IPaymentChannelClient { * @param conn A callback listener which represents the connection to the server (forwards messages we generate to * the server) */ - public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, ClientConnection conn) { - this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, null, conn); + public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, + ClientConnection conn) { + this(wallet,myKey,maxValue,serverId, conn, VersionSelector.VERSION_2_ALLOW_1); + } + + /** + * Constructs a new channel manager which waits for {@link PaymentChannelClient#connectionOpen()} before acting. + * A default time window of {@link #DEFAULT_TIME_WINDOW} will be used. + * + * @param wallet The wallet which will be paid from, and where completed transactions will be committed. + * Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set. + * @param myKey A freshly generated keypair used for the multisig contract and refund output. + * @param maxValue The maximum value the server is allowed to request that we lock into this channel until the + * refund transaction unlocks. Note that if there is a previously open channel, the refund + * transaction used in this channel may be larger than maxValue. Thus, maxValue is not a method for + * limiting the amount payable through this channel. + * @param serverId An arbitrary hash representing this channel. This must uniquely identify the server. If an + * existing stored channel exists in the wallet's {@link StoredPaymentChannelClientStates}, then an + * attempt will be made to resume that channel. + * @param conn A callback listener which represents the connection to the server (forwards messages we generate to + * the server) + * @param versionSelector An enum indicating which versions to support: + * VERSION_1: use only version 1 of the protocol + * VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1 + * VERSION_2: suggest version 2 and enforce use of version 2 + * + */ + public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, + ClientConnection conn, VersionSelector versionSelector) { + this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, null, conn, versionSelector); } /** @@ -155,6 +219,35 @@ public class PaymentChannelClient implements IPaymentChannelClient { */ public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, long timeWindow, @Nullable KeyParameter userKeySetup, ClientConnection conn) { + this(wallet, myKey, maxValue, serverId, timeWindow, userKeySetup, conn, VersionSelector.VERSION_2_ALLOW_1); + } + + /** + * Constructs a new channel manager which waits for {@link PaymentChannelClient#connectionOpen()} before acting. + * + * @param wallet The wallet which will be paid from, and where completed transactions will be committed. + * Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set. + * @param myKey A freshly generated keypair used for the multisig contract and refund output. + * @param maxValue The maximum value the server is allowed to request that we lock into this channel until the + * refund transaction unlocks. Note that if there is a previously open channel, the refund + * transaction used in this channel may be larger than maxValue. Thus, maxValue is not a method for + * limiting the amount payable through this channel. + * @param serverId An arbitrary hash representing this channel. This must uniquely identify the server. If an + * existing stored channel exists in the wallet's {@link StoredPaymentChannelClientStates}, then an + * attempt will be made to resume that channel. + * @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open. Note that is is + * a proposal to the server. The server may in turn propose something different. + * See {@link org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientConnection#acceptExpireTime(long)} + * @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup. + * @param conn A callback listener which represents the connection to the server (forwards messages we generate to + * the server) + * @param versionSelector An enum indicating which versions to support: + * VERSION_1: use only version 1 of the protocol + * VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1 + * VERSION_2: suggest version 2 and enforce use of version 2 + */ + public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, long timeWindow, + @Nullable KeyParameter userKeySetup, ClientConnection conn, VersionSelector versionSelector) { this.wallet = checkNotNull(wallet); this.myKey = checkNotNull(myKey); this.maxValue = checkNotNull(maxValue); @@ -163,6 +256,7 @@ public class PaymentChannelClient implements IPaymentChannelClient { this.timeWindow = timeWindow; this.conn = checkNotNull(conn); this.userKeySetup = userKeySetup; + this.versionSelector = versionSelector; } /** @@ -215,7 +309,16 @@ public class PaymentChannelClient implements IPaymentChannelClient { final byte[] pubKeyBytes = initiate.getMultisigKey().toByteArray(); if (!ECKey.isPubKeyCanonical(pubKeyBytes)) throw new VerificationException("Server gave us a non-canonical public key, protocol error."); - state = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes), contractValue, expireTime); + switch (majorVersion) { + case 1: + state = new PaymentChannelV1ClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes), contractValue, expireTime); + break; + case 2: + state = new PaymentChannelV2ClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes), contractValue, expireTime); + break; + default: + return CloseReason.NO_ACCEPTABLE_VERSION; + } try { state.initiate(userKeySetup); } catch (ValueOutOfRangeException e) { @@ -224,25 +327,62 @@ public class PaymentChannelClient implements IPaymentChannelClient { return CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE; } minPayment = initiate.getMinPayment(); - step = InitStep.WAITING_FOR_REFUND_RETURN; + switch (majorVersion) { + case 1: + step = InitStep.WAITING_FOR_REFUND_RETURN; - Protos.ProvideRefund.Builder provideRefundBuilder = Protos.ProvideRefund.newBuilder() - .setMultisigKey(ByteString.copyFrom(myKey.getPubKey())) - .setTx(ByteString.copyFrom(state.getIncompleteRefundTransaction().bitcoinSerialize())); + Protos.ProvideRefund.Builder provideRefundBuilder = Protos.ProvideRefund.newBuilder() + .setMultisigKey(ByteString.copyFrom(myKey.getPubKey())) + .setTx(ByteString.copyFrom(((PaymentChannelV1ClientState)state).getIncompleteRefundTransaction().bitcoinSerialize())); - conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() - .setProvideRefund(provideRefundBuilder) - .setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_REFUND) - .build()); + conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() + .setProvideRefund(provideRefundBuilder) + .setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_REFUND) + .build()); + break; + case 2: + step = InitStep.WAITING_FOR_CHANNEL_OPEN; + + // Before we can send the server the contract (ie send it to the network), we must ensure that our refund + // transaction is safely in the wallet - thus we store it (this also keeps it up-to-date when we pay) + state.storeChannelInWallet(serverId); + + Protos.ProvideContract.Builder provideContractBuilder = Protos.ProvideContract.newBuilder() + .setTx(ByteString.copyFrom(state.getContract().bitcoinSerialize())) + .setClientKey(ByteString.copyFrom(myKey.getPubKey())); + try { + // Make an initial payment of the dust limit, and put it into the message as well. The size of the + // server-requested dust limit was already sanity checked by this point. + PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(Coin.valueOf(minPayment), userKeySetup); + Protos.UpdatePayment.Builder initialMsg = provideContractBuilder.getInitialPaymentBuilder(); + initialMsg.setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin())); + initialMsg.setClientChangeValue(state.getValueRefunded().value); + } catch (ValueOutOfRangeException e) { + throw new IllegalStateException(e); // This cannot happen. + } + + // Not used any more + userKeySetup = null; + + final Protos.TwoWayChannelMessage.Builder msg = Protos.TwoWayChannelMessage.newBuilder(); + msg.setProvideContract(provideContractBuilder); + msg.setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_CONTRACT); + conn.sendToServer(msg.build()); + break; + default: + return CloseReason.NO_ACCEPTABLE_VERSION; + } return null; } @GuardedBy("lock") private void receiveRefund(Protos.TwoWayChannelMessage refundMsg, @Nullable KeyParameter userKey) throws VerificationException { + checkState(majorVersion == 1); checkState(step == InitStep.WAITING_FOR_REFUND_RETURN && refundMsg.hasReturnRefund()); log.info("Got RETURN_REFUND message, providing signed contract"); Protos.ReturnRefund returnedRefund = refundMsg.getReturnRefund(); - state.provideRefundSignature(returnedRefund.getSignature().toByteArray(), userKey); + // Cast is safe since we've checked the version number + ((PaymentChannelV1ClientState)state).provideRefundSignature(returnedRefund.getSignature().toByteArray(), userKey); step = InitStep.WAITING_FOR_CHANNEL_OPEN; // Before we can send the server the contract (ie send it to the network), we must ensure that our refund @@ -250,7 +390,7 @@ public class PaymentChannelClient implements IPaymentChannelClient { state.storeChannelInWallet(serverId); Protos.ProvideContract.Builder contractMsg = Protos.ProvideContract.newBuilder() - .setTx(ByteString.copyFrom(state.getMultisigContract().bitcoinSerialize())); + .setTx(ByteString.copyFrom(state.getContract().bitcoinSerialize())); try { // Make an initial payment of the dust limit, and put it into the message as well. The size of the // server-requested dust limit was already sanity checked by this point. @@ -277,7 +417,16 @@ public class PaymentChannelClient implements IPaymentChannelClient { if (step == InitStep.WAITING_FOR_INITIATE) { // We skipped the initiate step, because a previous channel that's still valid was resumed. wasInitiated = false; - state = new PaymentChannelClientState(storedChannel, wallet); + switch (majorVersion) { + case 1: + state = new PaymentChannelV1ClientState(storedChannel, wallet); + break; + case 2: + state = new PaymentChannelV2ClientState(storedChannel, wallet); + break; + default: + throw new IllegalStateException("Invalid version number " + majorVersion); + } } step = InitStep.CHANNEL_OPEN; // channelOpen should disable timeouts, but @@ -302,7 +451,8 @@ public class PaymentChannelClient implements IPaymentChannelClient { checkState(step == InitStep.WAITING_FOR_VERSION_NEGOTIATION && msg.hasServerVersion()); // Server might send back a major version lower than our own if they want to fallback to a // lower version. We can't handle that, so we just close the channel. - if (msg.getServerVersion().getMajor() != SERVER_MAJOR_VERSION) { + majorVersion = msg.getServerVersion().getMajor(); + if (!versionSelector.isServerVersionAccepted(majorVersion, msg.getServerVersion().getMinor())) { errorBuilder = Protos.Error.newBuilder() .setCode(Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION); closeReason = CloseReason.NO_ACCEPTABLE_VERSION; @@ -474,8 +624,8 @@ public class PaymentChannelClient implements IPaymentChannelClient { step = InitStep.WAITING_FOR_VERSION_NEGOTIATION; Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder() - .setMajor(CLIENT_MAJOR_VERSION) - .setMinor(CLIENT_MINOR_VERSION) + .setMajor(versionSelector.getRequestedMajorVersion()) + .setMinor(versionSelector.getRequestedMinorVersion()) .setTimeWindowSecs(timeWindow); if (storedChannel != null) { @@ -557,7 +707,7 @@ public class PaymentChannelClient implements IPaymentChannelClient { if (wallet.isEncrypted() && userKey == null) throw new ECKey.KeyIsEncryptedException(); - PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(size, userKey); + PaymentChannelV1ClientState.IncrementedPayment payment = state().incrementPaymentBy(size, userKey); Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder() .setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin())) .setClientChangeValue(state.getValueRefunded().value); diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java index fb598e391..a93f9621b 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java @@ -51,27 +51,86 @@ public class PaymentChannelClientConnection { * {@link org.bitcoinj.protocols.channels.PaymentChannelClient#DEFAULT_TIME_WINDOW} * seconds. If the server proposes a longer time the channel will be closed. * - * @param server The host/port pair where the server is listening. + * @param server The host/port pair where the server is listening. * @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough * to accommodate ECDSA signature operations and network latency. - * @param wallet The wallet which will be paid from, and where completed transactions will be committed. - * Must be unencrypted. Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set. - * @param myKey A freshly generated keypair used for the multisig contract and refund output. - * @param maxValue The maximum value this channel is allowed to request - * @param serverId A unique ID which is used to attempt reopening of an existing channel. - * This must be unique to the server, and, if your application is exposing payment channels to some - * API, this should also probably encompass some caller UID to avoid applications opening channels - * which were created by others. - * - * @throws IOException if there's an issue using the network. + * @param wallet The wallet which will be paid from, and where completed transactions will be committed. + * Must be unencrypted. Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set. + * @param myKey A freshly generated keypair used for the multisig contract and refund output. + * @param maxValue The maximum value this channel is allowed to request + * @param serverId A unique ID which is used to attempt reopening of an existing channel. + * This must be unique to the server, and, if your application is exposing payment channels to some + * API, this should also probably encompass some caller UID to avoid applications opening channels + * which were created by others. + * @throws IOException if there's an issue using the network. * @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue. */ public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey, Coin maxValue, String serverId) throws IOException, ValueOutOfRangeException { this(server, timeoutSeconds, wallet, myKey, maxValue, serverId, - PaymentChannelClient.DEFAULT_TIME_WINDOW, null); + PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1); } + /** + * Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the + * connection is open. The server is requested to keep the channel open for + * {@link org.bitcoinj.protocols.channels.PaymentChannelClient#DEFAULT_TIME_WINDOW} + * seconds. If the server proposes a longer time the channel will be closed. + * + * @param server The host/port pair where the server is listening. + * @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough + * to accommodate ECDSA signature operations and network latency. + * @param wallet The wallet which will be paid from, and where completed transactions will be committed. + * Must be unencrypted. Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set. + * @param myKey A freshly generated keypair used for the multisig contract and refund output. + * @param maxValue The maximum value this channel is allowed to request + * @param serverId A unique ID which is used to attempt reopening of an existing channel. + * This must be unique to the server, and, if your application is exposing payment channels to some + * API, this should also probably encompass some caller UID to avoid applications opening channels + * which were created by others. + * @param versionSelector An enum indicating which versions to support: + * VERSION_1: use only version 1 of the protocol + * VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1 + * VERSION_2: suggest version 2 and enforce use of version 2 + * @throws IOException if there's an issue using the network. + * @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue. + */ + public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey, + Coin maxValue, String serverId, PaymentChannelClient.VersionSelector versionSelector) throws IOException, ValueOutOfRangeException { + this(server, timeoutSeconds, wallet, myKey, maxValue, serverId, + PaymentChannelClient.DEFAULT_TIME_WINDOW, null, versionSelector); + } + + /** + * Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the + * connection is open. The server is requested to keep the channel open for {@param timeWindow} + * seconds. If the server proposes a longer time the channel will be closed. + * + * @param server The host/port pair where the server is listening. + * @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough + * to accommodate ECDSA signature operations and network latency. + * @param wallet The wallet which will be paid from, and where completed transactions will be committed. + * Can be encrypted if user key is supplied when needed. Must already have a + * {@link StoredPaymentChannelClientStates} object in its extensions set. + * @param myKey A freshly generated keypair used for the multisig contract and refund output. + * @param maxValue The maximum value this channel is allowed to request + * @param serverId A unique ID which is used to attempt reopening of an existing channel. + * This must be unique to the server, and, if your application is exposing payment channels to some + * API, this should also probably encompass some caller UID to avoid applications opening channels + * which were created by others. + * @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open. + * @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup. + * @throws IOException if there's an issue using the network. + * @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue. + */ + public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey, + Coin maxValue, String serverId, final long timeWindow, + @Nullable KeyParameter userKeySetup) throws IOException, ValueOutOfRangeException { + this(server, timeoutSeconds, wallet, myKey, maxValue, serverId, + timeWindow, userKeySetup, PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1); + } + + /** * Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the * connection is open. The server is requested to keep the channel open for {@param timeWindow} @@ -91,13 +150,17 @@ public class PaymentChannelClientConnection { * which were created by others. * @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open. * @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup. + * @param versionSelector An enum indicating which versions to support: + * VERSION_1: use only version 1 of the protocol + * VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1 + * VERSION_2: suggest version 2 and enforce use of version 2 * * @throws IOException if there's an issue using the network. * @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue. */ public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey, Coin maxValue, String serverId, final long timeWindow, - @Nullable KeyParameter userKeySetup) + @Nullable KeyParameter userKeySetup, PaymentChannelClient.VersionSelector versionSelector) throws IOException, ValueOutOfRangeException { // Glue the object which vends/ingests protobuf messages in order to manage state to the network object which // reads/writes them to the wire in length prefixed form. @@ -125,7 +188,7 @@ public class PaymentChannelClientConnection { // Inform the API user that we're done and ready to roll. channelOpenFuture.set(PaymentChannelClientConnection.this); } - }); + }, versionSelector); // And glue back in the opposite direction - network to the channelClient. wireParser = new ProtobufConnection
Gets the {@link PaymentChannelClientState} object which stores the current state of the connection with the + *
Gets the {@link PaymentChannelV1ClientState} object which stores the current state of the connection with the * server.
* *Note that if you call any methods which update state directly the server will not be notified and channel diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java index 5f6a5a921..01a4f3611 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java @@ -16,24 +16,22 @@ package org.bitcoinj.protocols.channels; -import org.bitcoinj.core.*; -import org.bitcoinj.crypto.TransactionSignature; -import org.bitcoinj.script.Script; -import org.bitcoinj.script.ScriptBuilder; -import org.bitcoinj.utils.Threading; -import org.bitcoinj.wallet.AllowUnconfirmedCoinSelector; -import org.spongycastle.crypto.params.KeyParameter; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; -import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.bitcoinj.core.*; +import org.bitcoinj.core.listeners.AbstractWalletEventListener; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.utils.Threading; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.spongycastle.crypto.params.KeyParameter; import javax.annotation.Nullable; -import java.util.List; import static com.google.common.base.Preconditions.*; import org.bitcoinj.core.listeners.WalletCoinsReceivedEventListener; @@ -44,6 +42,9 @@ import org.bitcoinj.core.listeners.WalletCoinsReceivedEventListener; * implement micropayments or other payment schemes in which immediate settlement is not required, but zero trust * negotiation is. Note that this class only allows the amount of money sent to be incremented, not decremented.
* + *This class has two subclasses, {@link PaymentChannelV1ClientState} and {@link PaymentChannelV2ClientState} for + * protocols version 1 and 2.
+ * *This class implements the core state machine for the client side of the protocol. The server side is implemented * by {@link PaymentChannelServerState} and {@link PaymentChannelClientConnection} implements a network protocol * suitable for TCP/IP connections which moves this class through each state. We say that the party who is sending funds @@ -58,38 +59,20 @@ import org.bitcoinj.core.listeners.WalletCoinsReceivedEventListener; * the given time (within a few hours), the channel must be closed or else the client will broadcast the refund * transaction and take back all the money once the expiry time is reached.
* - *To begin, the client calls {@link PaymentChannelClientState#initiate()}, which moves the channel into state + *
To begin, the client calls {@link PaymentChannelV1ClientState#initiate()}, which moves the channel into state * INITIATED and creates the initial multi-sig contract and refund transaction. If the wallet has insufficient funds an * exception will be thrown at this point. Once this is done, call - * {@link PaymentChannelClientState#getIncompleteRefundTransaction()} and pass the resultant transaction through to the - * server. Once you have retrieved the signature, use {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)}. - * You must then call {@link PaymentChannelClientState#storeChannelInWallet(Sha256Hash)} to store the refund transaction + * {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction()} and pass the resultant transaction through to the + * server. Once you have retrieved the signature, use {@link PaymentChannelV1ClientState#provideRefundSignature(byte[], KeyParameter)}. + * You must then call {@link PaymentChannelV1ClientState#storeChannelInWallet(Sha256Hash)} to store the refund transaction * in the wallet, protecting you against a malicious server attempting to destroy all your coins. At this point, you can - * provide the server with the multi-sig contract (via {@link PaymentChannelClientState#getMultisigContract()}) safely. + * provide the server with the multi-sig contract (via {@link PaymentChannelV1ClientState#getContract()}) safely. *
*/ -public class PaymentChannelClientState { +public abstract class PaymentChannelClientState { private static final Logger log = LoggerFactory.getLogger(PaymentChannelClientState.class); - - private final Wallet wallet; - // Both sides need a key (private in our case, public for the server) in order to manage the multisig contract - // and transactions that spend it. - private final ECKey myKey, serverMultisigKey; - // How much value (in satoshis) is locked up into the channel. - private final Coin totalValue; - // When the channel will automatically settle in favor of the client, if the server halts before protocol termination - // specified in terms of block timestamps (so it can off real time by a few hours). - private final long expiryTime; - - // The refund is a time locked transaction that spends all the money of the channel back to the client. - private Transaction refundTx; - private Coin refundFees; - // The multi-sig contract locks the value of the channel up such that the agreement of both parties is required - // to spend it. - private Transaction multisigContract; - private Script multisigScript; // How much value is currently allocated to us. Starts as being same as totalValue. - private Coin valueToMe; + protected Coin valueToMe; /** * The different logical states the channel can be in. The channel starts out as NEW, and then steps through the @@ -97,6 +80,7 @@ public class PaymentChannelClientState { * by the time the NEW state is reached. */ public enum State { + UNINITIALISED, NEW, INITIATED, WAITING_FOR_SIGNED_REFUND, @@ -106,26 +90,24 @@ public class PaymentChannelClientState { EXPIRED, CLOSED } - private State state; + protected final StateMachineWhen the servers signature for the refund transaction is received, call this to verify it and sign the - * complete refund ourselves.
- * - *If this does not throw an exception, we are secure against the loss of funds and can safely provide the server - * with the multi-sig contract to lock in the agreement. In this case, both the multisig contract and the refund - * transaction are automatically committed to wallet so that it can handle broadcasting the refund transaction at - * the appropriate time if necessary.
- */ - public synchronized void provideRefundSignature(byte[] theirSignature, @Nullable KeyParameter userKey) - throws VerificationException { - checkNotNull(theirSignature); - checkState(state == State.WAITING_FOR_SIGNED_REFUND); - TransactionSignature theirSig = TransactionSignature.decodeFromBitcoin(theirSignature, true); - if (theirSig.sigHashMode() != Transaction.SigHash.NONE || !theirSig.anyoneCanPay()) - throw new VerificationException("Refund signature was not SIGHASH_NONE|SIGHASH_ANYONECANPAY"); - // Sign the refund transaction ourselves. - final TransactionOutput multisigContractOutput = multisigContract.getOutput(0); - try { - multisigScript = multisigContractOutput.getScriptPubKey(); - } catch (ScriptException e) { - throw new RuntimeException(e); // Cannot happen: we built this ourselves. - } - TransactionSignature ourSignature = - refundTx.calculateSignature(0, myKey.maybeDecrypt(userKey), - multisigScript, Transaction.SigHash.ALL, false); - // Insert the signatures. - Script scriptSig = ScriptBuilder.createMultiSigInputScript(ourSignature, theirSig); - log.info("Refund scriptSig: {}", scriptSig); - log.info("Multi-sig contract scriptPubKey: {}", multisigScript); - TransactionInput refundInput = refundTx.getInput(0); - refundInput.setScriptSig(scriptSig); - refundInput.verify(multisigContractOutput); - state = State.SAVE_STATE_IN_WALLET; - } + public abstract Transaction getContract(); private synchronized Transaction makeUnsignedChannelContract(Coin valueToMe) throws ValueOutOfRangeException { Transaction tx = new Transaction(wallet.getParams()); - tx.addInput(multisigContract.getOutput(0)); + tx.addInput(getContractInternal().getOutput(0)); // Our output always comes first. // TODO: We should drop myKey in favor of output key + multisig key separation // (as its always obvious who the client is based on T2 output order) @@ -383,8 +263,8 @@ public class PaymentChannelClientState { * storage and throwing an {@link IllegalStateException} if it is. */ public synchronized void checkNotExpired() { - if (Utils.currentTimeSeconds() > expiryTime) { - state = State.EXPIRED; + if (Utils.currentTimeSeconds() > getExpiryTime()) { + stateMachine.transition(State.EXPIRED); disconnectFromChannel(); throw new IllegalStateException("Channel expired"); } @@ -404,8 +284,8 @@ public class PaymentChannelClientState { *The returned signature is over the payment transaction, which we never have a valid copy of and thus there * is no accessor for it on this object.
* - *To spend the whole channel increment by {@link PaymentChannelClientState#getTotalValue()} - - * {@link PaymentChannelClientState#getValueRefunded()}
+ *To spend the whole channel increment by {@link PaymentChannelV1ClientState#getTotalValue()} - + * {@link PaymentChannelV1ClientState#getValueRefunded()}
* * @param size How many satoshis to increment the payment by (note: not the new total). * @throws ValueOutOfRangeException If size is negative or the channel does not have sufficient money in it to @@ -413,15 +293,15 @@ public class PaymentChannelClientState { */ public synchronized IncrementedPayment incrementPaymentBy(Coin size, @Nullable KeyParameter userKey) throws ValueOutOfRangeException { - checkState(state == State.READY); + stateMachine.checkState(State.READY); checkNotExpired(); checkNotNull(size); // Validity of size will be checked by makeUnsignedChannelContract. if (size.signum() < 0) throw new ValueOutOfRangeException("Tried to decrement payment"); - Coin newValueToMe = valueToMe.subtract(size); + Coin newValueToMe = getValueToMe().subtract(size); if (newValueToMe.compareTo(Transaction.MIN_NONDUST_OUTPUT) < 0 && newValueToMe.signum() > 0) { log.info("New value being sent back as change was smaller than minimum nondust output, sending all"); - size = valueToMe; + size = getValueToMe(); newValueToMe = Coin.ZERO; } if (newValueToMe.signum() < 0) @@ -435,7 +315,7 @@ public class PaymentChannelClientState { mode = Transaction.SigHash.NONE; else mode = Transaction.SigHash.SINGLE; - TransactionSignature sig = tx.calculateSignature(0, myKey.maybeDecrypt(userKey), multisigScript, mode, true); + TransactionSignature sig = tx.calculateSignature(0, myKey.maybeDecrypt(userKey), getSignedScript(), mode, true); valueToMe = newValueToMe; updateChannelInWallet(); IncrementedPayment payment = new IncrementedPayment(); @@ -444,10 +324,10 @@ public class PaymentChannelClientState { return payment; } - private synchronized void updateChannelInWallet() { + protected synchronized void updateChannelInWallet() { if (storedChannel == null) return; - storedChannel.valueToMe = valueToMe; + storedChannel.valueToMe = getValueToMe(); StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); channels.updatedChannel(storedChannel); @@ -457,7 +337,7 @@ public class PaymentChannelClientState { * Sets this channel's state in {@link StoredPaymentChannelClientStates} to unopened so this channel can be reopened * later. * - * @see PaymentChannelClientState#storeChannelInWallet(Sha256Hash) + * @see PaymentChannelV1ClientState#storeChannelInWallet(Sha256Hash) */ public synchronized void disconnectFromChannel() { if (storedChannel == null) @@ -472,21 +352,14 @@ public class PaymentChannelClientState { */ @VisibleForTesting synchronized void fakeSave() { try { - wallet.commitTx(multisigContract); + wallet.commitTx(getContractInternal()); } catch (VerificationException e) { throw new RuntimeException(e); // We created it } - state = State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER; + stateMachine.transition(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER); } - @VisibleForTesting synchronized void doStoreChannelInWallet(Sha256Hash id) { - StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates) - wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); - checkNotNull(channels, "You have not added the StoredPaymentChannelClientStates extension to the wallet."); - checkState(channels.getChannel(id, multisigContract.getHash()) == null); - storedChannel = new StoredClientChannel(id, multisigContract, refundTx, myKey, valueToMe, refundFees, true); - channels.putChannel(storedChannel); - } + @VisibleForTesting abstract void doStoreChannelInWallet(Sha256Hash id); /** *Stores this channel's state in the wallet as a part of a {@link StoredPaymentChannelClientStates} wallet
@@ -501,7 +374,8 @@ public class PaymentChannelClientState {
* unique.
*/
public synchronized void storeChannelInWallet(Sha256Hash id) {
- checkState(state == State.SAVE_STATE_IN_WALLET && id != null);
+ stateMachine.checkState(State.SAVE_STATE_IN_WALLET);
+ checkState(id != null);
if (storedChannel != null) {
checkState(storedChannel.id.equals(id));
return;
@@ -509,44 +383,31 @@ public class PaymentChannelClientState {
doStoreChannelInWallet(id);
try {
- wallet.commitTx(multisigContract);
+ wallet.commitTx(getContractInternal());
} catch (VerificationException e) {
throw new RuntimeException(e); // We created it
}
- state = State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER;
+ stateMachine.transition(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER);
}
/**
* Returns the fees that will be paid if the refund transaction has to be claimed because the server failed to settle
- * the channel properly. May only be called after {@link PaymentChannelClientState#initiate()}
+ * the channel properly. May only be called after {@link PaymentChannelV1ClientState#initiate()}
*/
- public synchronized Coin getRefundTxFees() {
- checkState(state.compareTo(State.NEW) > 0);
- return refundFees;
- }
+ public abstract Coin getRefundTxFees();
- /**
- * Once the servers signature over the refund transaction has been received and provided using
- * {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)} then this
- * method can be called to receive the now valid and broadcastable refund transaction.
- */
- public synchronized Transaction getCompletedRefundTransaction() {
- checkState(state.compareTo(State.WAITING_FOR_SIGNED_REFUND) > 0);
- return refundTx;
- }
+ @VisibleForTesting abstract Transaction getRefundTransaction();
/**
* Gets the total value of this channel (ie the maximum payment possible)
*/
- public Coin getTotalValue() {
- return totalValue;
- }
+ public abstract Coin getTotalValue();
/**
* Gets the current amount refunded to us from the multisig contract (ie totalValue-valueSentToServer)
*/
public synchronized Coin getValueRefunded() {
- checkState(state == State.READY);
+ stateMachine.checkState(State.READY);
return valueToMe;
}
@@ -556,4 +417,23 @@ public class PaymentChannelClientState {
public synchronized Coin getValueSpent() {
return getTotalValue().subtract(getValueRefunded());
}
+
+ protected abstract Coin getValueToMe();
+
+ protected abstract long getExpiryTime();
+
+ /**
+ * Gets the contract without changing the state machine
+ * @return
+ */
+ protected abstract Transaction getContractInternal();
+
+ protected abstract Script getContractScript();
+
+ /**
+ * Gets the script that is signed. In the case of a P2SH contract this is the
+ * script inside the P2SH script.
+ * @return
+ */
+ protected abstract Script getSignedScript();
}
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java
index 97272e324..8ae91dcbc 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java
@@ -16,6 +16,7 @@
package org.bitcoinj.protocols.channels;
+import com.google.common.collect.ImmutableMap;
import org.bitcoinj.core.*;
import org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason;
import org.bitcoinj.utils.Threading;
@@ -28,6 +29,7 @@ import org.bitcoin.paymentchannel.Protos;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
+import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.checkNotNull;
@@ -47,12 +49,17 @@ public class PaymentChannelServer {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelServer.class);
protected final ReentrantLock lock = Threading.lock("channelserver");
- public final int SERVER_MAJOR_VERSION = 1;
- public final int SERVER_MINOR_VERSION = 0;
+
+ /**
+ * A map of supported versions; keys are major versions, and the corresponding
+ * value is the minor version at that major level.
+ */
+ public static final Map A payment channel is a method of sending money to someone such that the amount of money you send can be adjusted
@@ -41,8 +44,11 @@ import static com.google.common.base.Preconditions.*;
* implement micropayments or other payment schemes in which immediate settlement is not required, but zero trust
* negotiation is. Note that this class only allows the amount of money received to be incremented, not decremented. There are two subclasses that implement this one, for versions 1 and 2 of the protocol -
+ * {@link PaymentChannelV1ServerState} and {@link PaymentChannelV2ServerState}. This class implements the core state machine for the server side of the protocol. The client side is implemented
- * by {@link PaymentChannelClientState} and {@link PaymentChannelServerListener} implements the server-side network
+ * by {@link PaymentChannelV1ClientState} and {@link PaymentChannelServerListener} implements the server-side network
* protocol listening for TCP/IP connections and moving this class through each state. We say that the party who is
* sending funds is the client or initiating party. The party that is receiving the funds is the
* server or receiving party. Although the underlying Bitcoin protocol is capable of more complex
@@ -64,7 +70,7 @@ import static com.google.common.base.Preconditions.*;
* can then begin paying by providing us with signatures for the multi-sig contract which pay some amount back to the
* client, and the rest is ours to do with as we wish. Closes this channel and broadcasts the highest value payment transaction on the network. This will set the state to {@link State#CLOSED} if the transaction is successfully broadcast on the network.
- * If we fail to broadcast for some reason, the state is set to {@link State#ERROR}. If the current state is before {@link State#READY} (ie we have not finished initializing the channel), we
- * simply set the state to {@link State#CLOSED} and let the client handle getting its refund transaction confirmed.
- * When the servers signature for the refund transaction is received, call this to verify it and sign the
+ * complete refund ourselves. If this does not throw an exception, we are secure against the loss of funds and can safely provide the server
+ * with the multi-sig contract to lock in the agreement. In this case, both the multisig contract and the refund
+ * transaction are automatically committed to wallet so that it can handle broadcasting the refund transaction at
+ * the appropriate time if necessary. Closes this channel and broadcasts the highest value payment transaction on the network. This will set the state to {@link State#CLOSED} if the transaction is successfully broadcast on the network.
+ * If we fail to broadcast for some reason, the state is set to {@link State#ERROR}. If the current state is before {@link State#READY} (ie we have not finished initializing the channel), we
+ * simply set the state to {@link State#CLOSED} and let the client handle getting its refund transaction confirmed.
+ * Closes the given channel using {@link ServerConnectionEventHandler#closeChannel()} and
- * {@link PaymentChannelServerState#close()} to notify any connected client of channel closure and to complete and
+ * {@link PaymentChannelV1ServerState#close()} to notify any connected client of channel closure and to complete and
* broadcast the latest payment transaction. Removes the given channel from this set of {@link StoredServerChannel}s and notifies the wallet of a change to
@@ -223,11 +223,16 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
checkState(channel.refundTransactionUnlockTimeSecs > 0);
checkNotNull(channel.myKey.getPrivKeyBytes());
ServerState.StoredServerPaymentChannel.Builder channelBuilder = ServerState.StoredServerPaymentChannel.newBuilder()
+ .setMajorVersion(channel.majorVersion)
.setBestValueToMe(channel.bestValueToMe.value)
.setRefundTransactionUnlockTimeSecs(channel.refundTransactionUnlockTimeSecs)
.setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize()))
- .setClientOutput(ByteString.copyFrom(channel.clientOutput.bitcoinSerialize()))
.setMyKey(ByteString.copyFrom(channel.myKey.getPrivKeyBytes()));
+ if (channel.majorVersion == 1) {
+ channelBuilder.setClientOutput(ByteString.copyFrom(channel.clientOutput.bitcoinSerialize()));
+ } else {
+ channelBuilder.setClientKey(ByteString.copyFrom(channel.clientKey.getPubKey()));
+ }
if (channel.bestValueSignature != null)
channelBuilder.setBestValueSignature(ByteString.copyFrom(channel.bestValueSignature));
builder.addChannels(channelBuilder);
@@ -246,11 +251,21 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
ServerState.StoredServerPaymentChannels states = ServerState.StoredServerPaymentChannels.parseFrom(data);
NetworkParameters params = containingWallet.getParams();
for (ServerState.StoredServerPaymentChannel storedState : states.getChannelsList()) {
+ final int majorVersion = storedState.getMajorVersion();
+ TransactionOutput clientOutput = null;
+ ECKey clientKey = null;
+ if (majorVersion == 1) {
+ clientOutput = new TransactionOutput(params, null, storedState.getClientOutput().toByteArray(), 0);
+ } else {
+ clientKey = ECKey.fromPublicOnly(storedState.getClientKey().toByteArray());
+ }
StoredServerChannel channel = new StoredServerChannel(null,
+ majorVersion,
params.getDefaultSerializer().makeTransaction(storedState.getContractTransaction().toByteArray()),
- new TransactionOutput(params, null, storedState.getClientOutput().toByteArray(), 0),
+ clientOutput,
storedState.getRefundTransactionUnlockTimeSecs(),
ECKey.fromPrivate(storedState.getMyKey().toByteArray()),
+ clientKey,
Coin.valueOf(storedState.getBestValueToMe()),
storedState.hasBestValueSignature() ? storedState.getBestValueSignature().toByteArray() : null);
putChannel(channel);
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/StoredServerChannel.java b/core/src/main/java/org/bitcoinj/protocols/channels/StoredServerChannel.java
index 674f8813e..feffdbcfd 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/StoredServerChannel.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/StoredServerChannel.java
@@ -30,24 +30,32 @@ import static com.google.common.base.Preconditions.checkArgument;
* time approaches.
*/
public class StoredServerChannel {
+ /**
+ * Channel version number. Currently can only be version 1
+ */
+ int majorVersion;
Coin bestValueToMe;
byte[] bestValueSignature;
long refundTransactionUnlockTimeSecs;
Transaction contract;
TransactionOutput clientOutput;
ECKey myKey;
+ // Used in protocol v2 only
+ ECKey clientKey;
// In-memory pointer to the event handler which handles this channel if the client is connected.
// Used as a flag to prevent duplicate connections and to disconnect the channel if its expire time approaches.
private PaymentChannelServer connectedHandler = null;
PaymentChannelServerState state = null;
- StoredServerChannel(@Nullable PaymentChannelServerState state, Transaction contract, TransactionOutput clientOutput,
- long refundTransactionUnlockTimeSecs, ECKey myKey, Coin bestValueToMe, @Nullable byte[] bestValueSignature) {
+ StoredServerChannel(@Nullable PaymentChannelServerState state, int majorVersion, Transaction contract, TransactionOutput clientOutput,
+ long refundTransactionUnlockTimeSecs, ECKey myKey, ECKey clientKey, Coin bestValueToMe, @Nullable byte[] bestValueSignature) {
+ this.majorVersion = majorVersion;
this.contract = contract;
this.clientOutput = clientOutput;
this.refundTransactionUnlockTimeSecs = refundTransactionUnlockTimeSecs;
this.myKey = myKey;
+ this.clientKey = clientKey;
this.bestValueToMe = bestValueToMe;
this.bestValueSignature = bestValueSignature;
this.state = state;
@@ -96,8 +104,18 @@ public class StoredServerChannel {
* @param broadcaster The {@link TransactionBroadcaster} which will be used to broadcast contract/payment transactions.
*/
public synchronized PaymentChannelServerState getOrCreateState(Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException {
- if (state == null)
- state = new PaymentChannelServerState(this, wallet, broadcaster);
+ if (state == null) {
+ switch (majorVersion) {
+ case 1:
+ state = new PaymentChannelV1ServerState(this, wallet, broadcaster);
+ break;
+ case 2:
+ state = new PaymentChannelV2ServerState(this, wallet, broadcaster);
+ break;
+ default:
+ throw new IllegalStateException("Invalid version number found");
+ }
+ }
checkArgument(wallet == state.wallet);
return state;
}
@@ -106,12 +124,13 @@ public class StoredServerChannel {
public synchronized String toString() {
final String newline = String.format(Locale.US, "%n");
return String.format(Locale.US, "Stored server channel (%s)%n" +
+ " Version: %d%n" +
" Key: %s%n" +
" Value to me: %s%n" +
" Client output: %s%n" +
" Refund unlock: %s (%d unix time)%n" +
" Contract: %s%n",
- connectedHandler != null ? "connected" : "disconnected", myKey, bestValueToMe,
+ connectedHandler != null ? "connected" : "disconnected", majorVersion, myKey, bestValueToMe,
clientOutput, new Date(refundTransactionUnlockTimeSecs * 1000), refundTransactionUnlockTimeSecs,
contract.toString().replaceAll(newline, newline + " "));
}
diff --git a/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java b/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java
index 9cc6c55d1..9642409db 100644
--- a/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java
+++ b/core/src/main/java/org/bitcoinj/script/ScriptBuilder.java
@@ -457,10 +457,31 @@ public class ScriptBuilder {
return builder.build();
}
- public static Script createCLTVPaymentChannelInput(TransactionSignature from, TransactionSignature to) {
+ public static Script createCLTVPaymentChannelP2SHRefund(TransactionSignature signature, Script redeemScript) {
ScriptBuilder builder = new ScriptBuilder();
- builder.data(from.encodeToBitcoin());
- builder.data(to.encodeToBitcoin());
+ builder.data(signature.encodeToBitcoin());
+ builder.data(new byte[] { 0 }); // Use the CHECKLOCKTIMEVERIFY if branch
+ builder.data(redeemScript.getProgram());
+ return builder.build();
+ }
+
+ public static Script createCLTVPaymentChannelP2SHInput(byte[] from, byte[] to, Script redeemScript) {
+ ScriptBuilder builder = new ScriptBuilder();
+ builder.data(from);
+ builder.data(to);
+ builder.smallNum(1); // Use the CHECKLOCKTIMEVERIFY if branch
+ builder.data(redeemScript.getProgram());
+ return builder.build();
+ }
+
+ public static Script createCLTVPaymentChannelInput(TransactionSignature from, TransactionSignature to) {
+ return createCLTVPaymentChannelInput(from.encodeToBitcoin(), to.encodeToBitcoin());
+ }
+
+ public static Script createCLTVPaymentChannelInput(byte[] from, byte[] to) {
+ ScriptBuilder builder = new ScriptBuilder();
+ builder.data(from);
+ builder.data(to);
builder.smallNum(1); // Use the CHECKLOCKTIMEVERIFY if branch
return builder.build();
}
diff --git a/core/src/paymentchannel.proto b/core/src/paymentchannel.proto
index 761eabbbd..0b860e6e0 100644
--- a/core/src/paymentchannel.proto
+++ b/core/src/paymentchannel.proto
@@ -178,11 +178,17 @@ message ReturnRefund {
// Sent from the primary to the secondary to complete initialization.
message ProvideContract {
// The serialized bytes of the transaction in Satoshi format.
+ // For version 1:
// * It must be signed and completely valid and ready for broadcast (ie it includes the
// necessary fees) TODO: tell the client how much fee it needs
// * Its first output must be a 2-of-2 multisig output with the first pubkey being the
// primary's and the second being the secondary's (ie the script must be exactly "OP_2
// ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG")
+ // For version 2:
+ // * It must be signed and completely valid and ready for broadcast (ie it includes the
+ // necessary fees) TODO: tell the client how much fee it needs
+ // * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the
+ // primary's and the second being the secondary's.
required bytes tx = 1;
// To open the channel, an initial payment of the server-specified dust limit value must be
@@ -190,6 +196,12 @@ message ProvideContract {
// no payment tx having been provided at all, or a payment that is smaller than the dust
// limit being provided.
required UpdatePayment initial_payment = 2;
+
+ // This field is added in protocol version 2 to send the client public key to the server.
+ // In version 1 it isn't used.
+ // This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms
+ // are accepted. It is only used in the creation of the multisig contract.
+ optional bytes client_key = 3;
}
// This message can only be used by the primary after it has received a CHANNEL_OPEN message. It
diff --git a/core/src/storedclientpaymentchannel.proto b/core/src/storedclientpaymentchannel.proto
index 5ce252878..1ca384204 100644
--- a/core/src/storedclientpaymentchannel.proto
+++ b/core/src/storedclientpaymentchannel.proto
@@ -43,9 +43,15 @@ message StoredClientPaymentChannel {
// Deprecated, key is already stored in the wallet, and found using myPublicKey;
required bytes myKey = 4;
required uint64 valueToMe = 5;
+ // Fees required to refund the transaction.
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;
+ // 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;
+ optional uint32 majorVersion = 9 [default = 1];
+ // The expiry time of the CLTV lock. Only used in protocol v2.
+ optional uint64 expiryTime = 10;
+ // The server's public key. Only used in protocol v2.
+ optional bytes serverKey = 11;
}
\ No newline at end of file
diff --git a/core/src/storedserverpaymentchannel.proto b/core/src/storedserverpaymentchannel.proto
index 1ff58abc3..6187261fb 100644
--- a/core/src/storedserverpaymentchannel.proto
+++ b/core/src/storedserverpaymentchannel.proto
@@ -39,6 +39,9 @@ message StoredServerPaymentChannel {
optional bytes bestValueSignature = 2;
required uint64 refundTransactionUnlockTimeSecs = 3;
required bytes contractTransaction = 4;
- required bytes clientOutput = 5;
+ optional bytes clientOutput = 5;
required bytes myKey = 6;
+ optional uint32 majorVersion = 7 [default = 1];
+ // Protocol version 2 only - the P2SH hash doesn't contain the required key
+ optional bytes clientKey = 8;
}
\ No newline at end of file
diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java
index ac1113017..41a4a5680 100644
--- a/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java
+++ b/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java
@@ -29,16 +29,19 @@ import org.bitcoin.paymentchannel.Protos;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
-import javax.lang.model.type.ExecutableType;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -48,6 +51,7 @@ import static org.bitcoinj.testing.FakeTxBuilder.createFakeBlock;
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType;
import static org.junit.Assert.*;
+@RunWith(Parameterized.class)
public class ChannelConnectionTest extends TestWithWallet {
private static final int CLIENT_MAJOR_VERSION = 1;
private Wallet serverWallet;
@@ -64,6 +68,35 @@ public class ChannelConnectionTest extends TestWithWallet {
}
};
+ /**
+ * We use parameterized tests to run the channel connection tests with each
+ * version of the channel.
+ */
+ @Parameterized.Parameters(name = "{index}: ChannelConnectionTest({0})")
+ public static Collectionrequired bytes clientOutput = 5;
+ * optional bytes clientOutput = 5;
*/
boolean hasClientOutput();
/**
- * required bytes clientOutput = 5;
+ * optional bytes clientOutput = 5;
*/
com.google.protobuf.ByteString getClientOutput();
@@ -761,6 +761,32 @@ public final class ServerState {
* required bytes myKey = 6;
*/
com.google.protobuf.ByteString getMyKey();
+
+ /**
+ * optional uint32 majorVersion = 7 [default = 1];
+ */
+ boolean hasMajorVersion();
+ /**
+ * optional uint32 majorVersion = 7 [default = 1];
+ */
+ int getMajorVersion();
+
+ /**
+ * optional bytes clientKey = 8;
+ *
+ *
+ * Protocol version 2 only - the P2SH hash doesn't contain the required key
+ *
+ */
+ boolean hasClientKey();
+ /**
+ * optional bytes clientKey = 8;
+ *
+ *
+ * Protocol version 2 only - the P2SH hash doesn't contain the required key
+ *
+ */
+ com.google.protobuf.ByteString getClientKey();
}
/**
* Protobuf type {@code paymentchannels.StoredServerPaymentChannel}
@@ -848,6 +874,16 @@ public final class ServerState {
myKey_ = input.readBytes();
break;
}
+ case 56: {
+ bitField0_ |= 0x00000040;
+ majorVersion_ = input.readUInt32();
+ break;
+ }
+ case 66: {
+ bitField0_ |= 0x00000080;
+ clientKey_ = input.readBytes();
+ break;
+ }
}
}
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
@@ -951,13 +987,13 @@ public final class ServerState {
public static final int CLIENTOUTPUT_FIELD_NUMBER = 5;
private com.google.protobuf.ByteString clientOutput_;
/**
- * required bytes clientOutput = 5;
+ * optional bytes clientOutput = 5;
*/
public boolean hasClientOutput() {
return ((bitField0_ & 0x00000010) == 0x00000010);
}
/**
- * required bytes clientOutput = 5;
+ * optional bytes clientOutput = 5;
*/
public com.google.protobuf.ByteString getClientOutput() {
return clientOutput_;
@@ -978,6 +1014,44 @@ public final class ServerState {
return myKey_;
}
+ public static final int MAJORVERSION_FIELD_NUMBER = 7;
+ private int majorVersion_;
+ /**
+ * optional uint32 majorVersion = 7 [default = 1];
+ */
+ public boolean hasMajorVersion() {
+ return ((bitField0_ & 0x00000040) == 0x00000040);
+ }
+ /**
+ * optional uint32 majorVersion = 7 [default = 1];
+ */
+ public int getMajorVersion() {
+ return majorVersion_;
+ }
+
+ public static final int CLIENTKEY_FIELD_NUMBER = 8;
+ private com.google.protobuf.ByteString clientKey_;
+ /**
+ * optional bytes clientKey = 8;
+ *
+ *
+ * Protocol version 2 only - the P2SH hash doesn't contain the required key
+ *
+ */
+ public boolean hasClientKey() {
+ return ((bitField0_ & 0x00000080) == 0x00000080);
+ }
+ /**
+ * optional bytes clientKey = 8;
+ *
+ *
+ * Protocol version 2 only - the P2SH hash doesn't contain the required key
+ *
+ */
+ public com.google.protobuf.ByteString getClientKey() {
+ return clientKey_;
+ }
+
private void initFields() {
bestValueToMe_ = 0L;
bestValueSignature_ = com.google.protobuf.ByteString.EMPTY;
@@ -985,6 +1059,8 @@ public final class ServerState {
contractTransaction_ = com.google.protobuf.ByteString.EMPTY;
clientOutput_ = com.google.protobuf.ByteString.EMPTY;
myKey_ = com.google.protobuf.ByteString.EMPTY;
+ majorVersion_ = 1;
+ clientKey_ = com.google.protobuf.ByteString.EMPTY;
}
private byte memoizedIsInitialized = -1;
public final boolean isInitialized() {
@@ -1004,10 +1080,6 @@ public final class ServerState {
memoizedIsInitialized = 0;
return false;
}
- if (!hasClientOutput()) {
- memoizedIsInitialized = 0;
- return false;
- }
if (!hasMyKey()) {
memoizedIsInitialized = 0;
return false;
@@ -1037,6 +1109,12 @@ public final class ServerState {
if (((bitField0_ & 0x00000020) == 0x00000020)) {
output.writeBytes(6, myKey_);
}
+ if (((bitField0_ & 0x00000040) == 0x00000040)) {
+ output.writeUInt32(7, majorVersion_);
+ }
+ if (((bitField0_ & 0x00000080) == 0x00000080)) {
+ output.writeBytes(8, clientKey_);
+ }
getUnknownFields().writeTo(output);
}
@@ -1070,6 +1148,14 @@ public final class ServerState {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(6, myKey_);
}
+ if (((bitField0_ & 0x00000040) == 0x00000040)) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeUInt32Size(7, majorVersion_);
+ }
+ if (((bitField0_ & 0x00000080) == 0x00000080)) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeBytesSize(8, clientKey_);
+ }
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
@@ -1203,6 +1289,10 @@ public final class ServerState {
bitField0_ = (bitField0_ & ~0x00000010);
myKey_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000020);
+ majorVersion_ = 1;
+ bitField0_ = (bitField0_ & ~0x00000040);
+ clientKey_ = com.google.protobuf.ByteString.EMPTY;
+ bitField0_ = (bitField0_ & ~0x00000080);
return this;
}
@@ -1255,6 +1345,14 @@ public final class ServerState {
to_bitField0_ |= 0x00000020;
}
result.myKey_ = myKey_;
+ if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
+ to_bitField0_ |= 0x00000040;
+ }
+ result.majorVersion_ = majorVersion_;
+ if (((from_bitField0_ & 0x00000080) == 0x00000080)) {
+ to_bitField0_ |= 0x00000080;
+ }
+ result.clientKey_ = clientKey_;
result.bitField0_ = to_bitField0_;
onBuilt();
return result;
@@ -1289,6 +1387,12 @@ public final class ServerState {
if (other.hasMyKey()) {
setMyKey(other.getMyKey());
}
+ if (other.hasMajorVersion()) {
+ setMajorVersion(other.getMajorVersion());
+ }
+ if (other.hasClientKey()) {
+ setClientKey(other.getClientKey());
+ }
this.mergeUnknownFields(other.getUnknownFields());
return this;
}
@@ -1306,10 +1410,6 @@ public final class ServerState {
return false;
}
- if (!hasClientOutput()) {
-
- return false;
- }
if (!hasMyKey()) {
return false;
@@ -1472,19 +1572,19 @@ public final class ServerState {
private com.google.protobuf.ByteString clientOutput_ = com.google.protobuf.ByteString.EMPTY;
/**
- * required bytes clientOutput = 5;
+ * optional bytes clientOutput = 5;
*/
public boolean hasClientOutput() {
return ((bitField0_ & 0x00000010) == 0x00000010);
}
/**
- * required bytes clientOutput = 5;
+ * optional bytes clientOutput = 5;
*/
public com.google.protobuf.ByteString getClientOutput() {
return clientOutput_;
}
/**
- * required bytes clientOutput = 5;
+ * optional bytes clientOutput = 5;
*/
public Builder setClientOutput(com.google.protobuf.ByteString value) {
if (value == null) {
@@ -1496,7 +1596,7 @@ public final class ServerState {
return this;
}
/**
- * required bytes clientOutput = 5;
+ * optional bytes clientOutput = 5;
*/
public Builder clearClientOutput() {
bitField0_ = (bitField0_ & ~0x00000010);
@@ -1540,6 +1640,89 @@ public final class ServerState {
return this;
}
+ private int majorVersion_ = 1;
+ /**
+ * optional uint32 majorVersion = 7 [default = 1];
+ */
+ public boolean hasMajorVersion() {
+ return ((bitField0_ & 0x00000040) == 0x00000040);
+ }
+ /**
+ * optional uint32 majorVersion = 7 [default = 1];
+ */
+ public int getMajorVersion() {
+ return majorVersion_;
+ }
+ /**
+ * optional uint32 majorVersion = 7 [default = 1];
+ */
+ public Builder setMajorVersion(int value) {
+ bitField0_ |= 0x00000040;
+ majorVersion_ = value;
+ onChanged();
+ return this;
+ }
+ /**
+ * optional uint32 majorVersion = 7 [default = 1];
+ */
+ public Builder clearMajorVersion() {
+ bitField0_ = (bitField0_ & ~0x00000040);
+ majorVersion_ = 1;
+ onChanged();
+ return this;
+ }
+
+ private com.google.protobuf.ByteString clientKey_ = com.google.protobuf.ByteString.EMPTY;
+ /**
+ * optional bytes clientKey = 8;
+ *
+ *
+ * Protocol version 2 only - the P2SH hash doesn't contain the required key
+ *
+ */
+ public boolean hasClientKey() {
+ return ((bitField0_ & 0x00000080) == 0x00000080);
+ }
+ /**
+ * optional bytes clientKey = 8;
+ *
+ *
+ * Protocol version 2 only - the P2SH hash doesn't contain the required key
+ *
+ */
+ public com.google.protobuf.ByteString getClientKey() {
+ return clientKey_;
+ }
+ /**
+ * optional bytes clientKey = 8;
+ *
+ *
+ * Protocol version 2 only - the P2SH hash doesn't contain the required key
+ *
+ */
+ public Builder setClientKey(com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ bitField0_ |= 0x00000080;
+ clientKey_ = value;
+ onChanged();
+ return this;
+ }
+ /**
+ * optional bytes clientKey = 8;
+ *
+ *
+ * Protocol version 2 only - the P2SH hash doesn't contain the required key
+ *
+ */
+ public Builder clearClientKey() {
+ bitField0_ = (bitField0_ & ~0x00000080);
+ clientKey_ = getDefaultInstance().getClientKey();
+ onChanged();
+ return this;
+ }
+
// @@protoc_insertion_point(builder_scope:paymentchannels.StoredServerPaymentChannel)
}
@@ -1573,13 +1756,14 @@ public final class ServerState {
"\n storedserverpaymentchannel.proto\022\017paym" +
"entchannels\"\\\n\033StoredServerPaymentChanne" +
"ls\022=\n\010channels\030\001 \003(\0132+.paymentchannels.S" +
- "toredServerPaymentChannel\"\272\001\n\032StoredServ" +
+ "toredServerPaymentChannel\"\346\001\n\032StoredServ" +
"erPaymentChannel\022\025\n\rbestValueToMe\030\001 \002(\004\022" +
"\032\n\022bestValueSignature\030\002 \001(\014\022\'\n\037refundTra" +
"nsactionUnlockTimeSecs\030\003 \002(\004\022\033\n\023contract" +
- "Transaction\030\004 \002(\014\022\024\n\014clientOutput\030\005 \002(\014\022" +
- "\r\n\005myKey\030\006 \002(\014B.\n\037org.bitcoinj.protocols" +
- ".channelsB\013ServerState"
+ "Transaction\030\004 \002(\014\022\024\n\014clientOutput\030\005 \001(\014\022" +
+ "\r\n\005myKey\030\006 \002(\014\022\027\n\014majorVersion\030\007 \001(\r:\0011\022" +
+ "\021\n\tclientKey\030\010 \001(\014B.\n\037org.bitcoinj.proto",
+ "cols.channelsB\013ServerState"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() {
@@ -1604,7 +1788,7 @@ public final class ServerState {
internal_static_paymentchannels_StoredServerPaymentChannel_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_paymentchannels_StoredServerPaymentChannel_descriptor,
- new java.lang.String[] { "BestValueToMe", "BestValueSignature", "RefundTransactionUnlockTimeSecs", "ContractTransaction", "ClientOutput", "MyKey", });
+ new java.lang.String[] { "BestValueToMe", "BestValueSignature", "RefundTransactionUnlockTimeSecs", "ContractTransaction", "ClientOutput", "MyKey", "MajorVersion", "ClientKey", });
}
// @@protoc_insertion_point(outer_class_scope)
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/StateMachine.java b/core/src/main/java/org/bitcoinj/protocols/channels/StateMachine.java
new file mode 100644
index 000000000..26a062724
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/StateMachine.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.bitcoinj.protocols.channels;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+
+import java.util.Locale;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A class representing a state machine, with limited transitions between states.
+ * @param true
if we are using a protocol version that requires the exchange of refunds.
+ */
+ private boolean useRefunds() {
+ return versionSelector == PaymentChannelClient.VersionSelector.VERSION_1;
+ }
+
+ /**
+ * Returns true
if the contract being used is a multisig contract
+ * @return
+ */
+ private boolean isMultiSigContract() {
+ return versionSelector == PaymentChannelClient.VersionSelector.VERSION_1;
+ }
+
@Override
@Before
public void setUp() throws Exception {
@@ -132,7 +165,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void exectuteSimpleChannelTest(KeyParameter userKeySetup) throws Exception {
// Test with network code and without any issues. We'll broadcast two txns: multisig contract and settle transaction.
- final SettableFuturetrue
if we are using a protocol version that requires the exchange of refunds.
+ */
+ private boolean useRefunds() {
+ return versionSelector == PaymentChannelClient.VersionSelector.VERSION_1;
+ }
+
private static class TxFuturePair {
Transaction tx;
SettableFuture