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(new ProtobufConnection.Listener() { @@ -217,7 +280,7 @@ public class PaymentChannelClientConnection { } /** - *

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 StateMachine stateMachine; + + 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. + final ECKey myKey, serverKey; // The id of this channel in the StoredPaymentChannelClientStates, or null if it is not stored - private StoredClientChannel storedChannel; + protected StoredClientChannel storedChannel; PaymentChannelClientState(StoredClientChannel storedClientChannel, Wallet wallet) throws VerificationException { - // The PaymentChannelClientConnection handles storedClientChannel.active and ensures we aren't resuming channels + this.stateMachine = new StateMachine(State.UNINITIALISED, getStateTransitions()); this.wallet = checkNotNull(wallet); - this.multisigContract = checkNotNull(storedClientChannel.contract); - this.multisigScript = multisigContract.getOutput(0).getScriptPubKey(); - this.refundTx = checkNotNull(storedClientChannel.refund); - this.refundFees = checkNotNull(storedClientChannel.refundFees); - this.expiryTime = refundTx.getLockTime(); this.myKey = checkNotNull(storedClientChannel.myKey); - this.serverMultisigKey = null; - this.totalValue = multisigContract.getOutput(0).getValue(); - this.valueToMe = checkNotNull(storedClientChannel.valueToMe); + this.serverKey = checkNotNull(storedClientChannel.serverKey); this.storedChannel = storedClientChannel; - this.state = State.READY; - initWalletListeners(); + this.valueToMe = checkNotNull(storedClientChannel.valueToMe); } /** @@ -134,7 +116,7 @@ public class PaymentChannelClientState { public synchronized boolean isSettlementTransaction(Transaction tx) { try { tx.verify(); - tx.getInput(0).verify(multisigContract.getOutput(0)); + tx.getInput(0).verify(getContractInternal().getOutput(0)); return true; } catch (VerificationException e) { return false; @@ -143,32 +125,29 @@ public class PaymentChannelClientState { /** * Creates a state object for a payment channel client. It is expected that you be ready to - * {@link PaymentChannelClientState#initiate()} after construction (to avoid creating objects for channels which are + * {@link PaymentChannelV1ClientState#initiate()} after construction (to avoid creating objects for channels which are * not going to finish opening) and thus some parameters provided here are only used in - * {@link PaymentChannelClientState#initiate()} to create the Multisig contract and refund transaction. + * {@link PaymentChannelV1ClientState#initiate()} to create the Multisig contract and refund transaction. * * @param wallet a wallet that contains at least the specified amount of value. * @param myKey a freshly generated private key for this channel. - * @param serverMultisigKey a public key retrieved from the server used for the initial multisig contract + * @param serverKey a public key retrieved from the server used for the initial multisig contract * @param value how many satoshis to put into this contract. If the channel reaches this limit, it must be closed. * It is suggested you use at least {@link Coin#CENT} to avoid paying fees if you need to spend the refund transaction * @param expiryTimeInSeconds At what point (UNIX timestamp +/- a few hours) the channel will expire * - * @throws VerificationException If either myKey's pubkey or serverMultisigKey's pubkey are non-canonical (ie invalid) + * @throws VerificationException If either myKey's pubkey or serverKey's pubkey are non-canonical (ie invalid) */ - public PaymentChannelClientState(Wallet wallet, ECKey myKey, ECKey serverMultisigKey, + public PaymentChannelClientState(Wallet wallet, ECKey myKey, ECKey serverKey, Coin value, long expiryTimeInSeconds) throws VerificationException { - checkArgument(value.signum() > 0); + this.stateMachine = new StateMachine(State.UNINITIALISED, getStateTransitions()); this.wallet = checkNotNull(wallet); - initWalletListeners(); - this.serverMultisigKey = checkNotNull(serverMultisigKey); + this.serverKey = checkNotNull(serverKey); this.myKey = checkNotNull(myKey); - this.valueToMe = this.totalValue = checkNotNull(value); - this.expiryTime = expiryTimeInSeconds; - this.state = State.NEW; + this.valueToMe = checkNotNull(value); } - private synchronized void initWalletListeners() { + protected synchronized void initWalletListeners() { // Register a listener that watches out for the server closing the channel. if (storedChannel != null && storedChannel.close != null) { watchCloseConfirmations(); @@ -177,11 +156,11 @@ public class PaymentChannelClientState { @Override public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { synchronized (PaymentChannelClientState.this) { - if (multisigContract == null) return; + if (getContractInternal() == null) return; if (isSettlementTransaction(tx)) { - log.info("Close: transaction {} closed contract {}", tx.getHash(), multisigContract.getHash()); + log.info("Close: transaction {} closed contract {}", tx.getHash(), getContractInternal().getHash()); // Record the fact that it was closed along with the transaction that closed it. - state = State.CLOSED; + stateMachine.transition(State.CLOSED); if (storedChannel == null) return; storedChannel.close = tx; updateChannelInWallet(); @@ -192,7 +171,7 @@ public class PaymentChannelClientState { }); } - private void watchCloseConfirmations() { + protected void watchCloseConfirmations() { // When we see the close transaction get enough confirmations, we can just delete the record // of this channel along with the refund tx from the wallet, because we're not going to need // any of that any more. @@ -220,18 +199,19 @@ public class PaymentChannelClientState { storedChannel = null; } - /** - * This object implements a state machine, and this accessor returns which state it's currently in. - */ public synchronized State getState() { - return state; + return stateMachine.getState(); } + protected abstract Multimap getStateTransitions(); + + public abstract int getMajorVersion(); + /** * Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate - * time using {@link PaymentChannelClientState#getIncompleteRefundTransaction} and - * {@link PaymentChannelClientState#getMultisigContract()}. The way the contract is crafted can be adjusted by - * overriding {@link PaymentChannelClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}. + * time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and + * {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by + * overriding {@link PaymentChannelV1ClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}. * By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low. * * @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network @@ -243,9 +223,9 @@ public class PaymentChannelClientState { /** * Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate - * time using {@link PaymentChannelClientState#getIncompleteRefundTransaction} and - * {@link PaymentChannelClientState#getMultisigContract()}. The way the contract is crafted can be adjusted by - * overriding {@link PaymentChannelClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}. + * time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and + * {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by + * overriding {@link PaymentChannelV1ClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}. * By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low. * @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted. * The wallet KeyCrypter is assumed. @@ -253,52 +233,7 @@ public class PaymentChannelClientState { * @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network * @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate */ - public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException { - final NetworkParameters params = wallet.getParams(); - Transaction template = new Transaction(params); - // We always place the client key before the server key because, if either side wants some privacy, they can - // use a fresh key for the the multisig contract and nowhere else - List keys = Lists.newArrayList(myKey, serverMultisigKey); - // There is also probably a change output, but we don't bother shuffling them as it's obvious from the - // format which one is the change. If we start obfuscating the change output better in future this may - // be worth revisiting. - TransactionOutput multisigOutput = template.addOutput(totalValue, ScriptBuilder.createMultiSigOutputScript(2, keys)); - if (multisigOutput.getMinNonDustValue().compareTo(totalValue) > 0) - throw new ValueOutOfRangeException("totalValue too small to use"); - Wallet.SendRequest req = Wallet.SendRequest.forTx(template); - req.coinSelector = AllowUnconfirmedCoinSelector.get(); - editContractSendRequest(req); - req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable. - req.aesKey = userKey; - wallet.completeTx(req); - Coin multisigFee = req.tx.getFee(); - multisigContract = req.tx; - // Build a refund transaction that protects us in the case of a bad server that's just trying to cause havoc - // by locking up peoples money (perhaps as a precursor to a ransom attempt). We time lock it so the server - // has an assurance that we cannot take back our money by claiming a refund before the channel closes - this - // relies on the fact that since Bitcoin 0.8 time locked transactions are non-final. This will need to change - // in future as it breaks the intended design of timelocking/tx replacement, but for now it simplifies this - // specific protocol somewhat. - refundTx = new Transaction(params); - refundTx.addInput(multisigOutput).setSequenceNumber(0); // Allow replacement when it's eventually reactivated. - refundTx.setLockTime(expiryTime); - if (totalValue.compareTo(Coin.CENT) < 0) { - // Must pay min fee. - final Coin valueAfterFee = totalValue.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); - if (Transaction.MIN_NONDUST_OUTPUT.compareTo(valueAfterFee) > 0) - throw new ValueOutOfRangeException("totalValue too small to use"); - refundTx.addOutput(valueAfterFee, myKey.toAddress(params)); - refundFees = multisigFee.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); - } else { - refundTx.addOutput(totalValue, myKey.toAddress(params)); - refundFees = multisigFee; - } - refundTx.getConfidence().setSource(TransactionConfidence.Source.SELF); - log.info("initiated channel with multi-sig contract {}, refund {}", multisigContract.getHashAsString(), - refundTx.getHashAsString()); - state = State.INITIATED; - // Client should now call getIncompleteRefundTransaction() and send it to the server. - } + public abstract void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException; /** * You can override this method in order to control the construction of the initial contract that creates the @@ -309,68 +244,13 @@ public class PaymentChannelClientState { } /** - * Returns the transaction that locks the money to the agreement of both parties. Do not mutate the result. - * Once this step is done, you can use {@link PaymentChannelClientState#incrementPaymentBy(Coin, KeyParameter)} to - * start paying the server. + * Gets the contract which was used to initialize this channel */ - public synchronized Transaction getMultisigContract() { - checkState(multisigContract != null); - if (state == State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER) - state = State.READY; - return multisigContract; - } - - /** - * Returns a partially signed (invalid) refund transaction that should be passed to the server. Once the server - * has checked it out and provided its own signature, call - * {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)} with the result. - */ - public synchronized Transaction getIncompleteRefundTransaction() { - checkState(refundTx != null); - if (state == State.INITIATED) - state = State.WAITING_FOR_SIGNED_REFUND; - return refundTx; - } - - /** - *

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.

- */ - 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 SERVER_VERSIONS = ImmutableMap.of(1, 0, 2, 0); // The step in the initialization process we are in, some of this is duplicated in the PaymentChannelServerState private enum InitStep { WAITING_ON_CLIENT_VERSION, + // This step is only used in V1 of the protocol. WAITING_ON_UNSIGNED_REFUND, WAITING_ON_CONTRACT, WAITING_ON_MULTISIG_ACCEPTANCE, @@ -111,6 +118,9 @@ public class PaymentChannelServer { } private final ServerConnection conn; + // Used to track the negotiated version number + @GuardedBy("lock") private int majorVersion; + // Used to keep track of whether or not the "socket" ie connection is open and we can generate messages @GuardedBy("lock") private boolean connectionOpen = false; // Indicates that no further messages should be sent and we intend to settle the connection @@ -211,15 +221,15 @@ public class PaymentChannelServer { private void receiveVersionMessage(Protos.TwoWayChannelMessage msg) throws VerificationException { checkState(step == InitStep.WAITING_ON_CLIENT_VERSION && msg.hasClientVersion()); final Protos.ClientVersion clientVersion = msg.getClientVersion(); - final int major = clientVersion.getMajor(); - if (major != SERVER_MAJOR_VERSION) { - error("This server needs protocol version " + SERVER_MAJOR_VERSION + " , client offered " + major, + majorVersion = clientVersion.getMajor(); + if (!SERVER_VERSIONS.containsKey(majorVersion)) { + error("This server needs one of protocol versions " + SERVER_VERSIONS.keySet() + " , client offered " + majorVersion, Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION, CloseReason.NO_ACCEPTABLE_VERSION); return; } Protos.ServerVersion.Builder versionNegotiationBuilder = Protos.ServerVersion.newBuilder() - .setMajor(SERVER_MAJOR_VERSION).setMinor(SERVER_MINOR_VERSION); + .setMajor(majorVersion).setMinor(SERVER_VERSIONS.get(majorVersion)); conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder() .setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION) .setServerVersion(versionNegotiationBuilder) @@ -262,7 +272,17 @@ public class PaymentChannelServer { wallet.freshReceiveKey(); expireTime = Utils.currentTimeSeconds() + truncateTimeWindow(clientVersion.getTimeWindowSecs()); - step = InitStep.WAITING_ON_UNSIGNED_REFUND; + switch (majorVersion) { + case 1: + step = InitStep.WAITING_ON_UNSIGNED_REFUND; + break; + case 2: + step = InitStep.WAITING_ON_CONTRACT; + break; + default: + error("Protocol version " + majorVersion + " not supported", Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION, CloseReason.NO_ACCEPTABLE_VERSION); + break; + } Protos.Initiate.Builder initiateBuilder = Protos.Initiate.newBuilder() .setMultisigKey(ByteString.copyFrom(myKey.getPubKey())) @@ -290,13 +310,16 @@ public class PaymentChannelServer { @GuardedBy("lock") private void receiveRefundMessage(Protos.TwoWayChannelMessage msg) throws VerificationException { + checkState(majorVersion == 1); checkState(step == InitStep.WAITING_ON_UNSIGNED_REFUND && msg.hasProvideRefund()); log.info("Got refund transaction, returning signature"); Protos.ProvideRefund providedRefund = msg.getProvideRefund(); - state = new PaymentChannelServerState(broadcaster, wallet, myKey, expireTime); - byte[] signature = state.provideRefundTransaction(wallet.getParams().getDefaultSerializer().makeTransaction(providedRefund.getTx().toByteArray()), - providedRefund.getMultisigKey().toByteArray()); + state = new PaymentChannelV1ServerState(broadcaster, wallet, myKey, expireTime); + // We can cast to V1 state since this state is only used in the V1 protocol + byte[] signature = ((PaymentChannelV1ServerState) state) + .provideRefundTransaction(wallet.getParams().getDefaultSerializer().makeTransaction(providedRefund.getTx().toByteArray()), + providedRefund.getMultisigKey().toByteArray()); step = InitStep.WAITING_ON_CONTRACT; @@ -344,18 +367,25 @@ public class PaymentChannelServer { @GuardedBy("lock") private void receiveContractMessage(Protos.TwoWayChannelMessage msg) throws VerificationException { + checkState(majorVersion == 1 || majorVersion == 2); checkState(step == InitStep.WAITING_ON_CONTRACT && msg.hasProvideContract()); log.info("Got contract, broadcasting and responding with CHANNEL_OPEN"); final Protos.ProvideContract providedContract = msg.getProvideContract(); + if (majorVersion == 2) { + state = new PaymentChannelV2ServerState(broadcaster, wallet, myKey, expireTime); + checkState(providedContract.hasClientKey(), "ProvideContract didn't have a client key in protocol v2"); + ((PaymentChannelV2ServerState)state).provideClientKey(providedContract.getClientKey().toByteArray()); + } + //TODO notify connection handler that timeout should be significantly extended as we wait for network propagation? - final Transaction multisigContract = wallet.getParams().getDefaultSerializer().makeTransaction(providedContract.getTx().toByteArray()); + final Transaction contract = wallet.getParams().getDefaultSerializer().makeTransaction(providedContract.getTx().toByteArray()); step = InitStep.WAITING_ON_MULTISIG_ACCEPTANCE; - state.provideMultiSigContract(multisigContract) + state.provideContract(contract) .addListener(new Runnable() { @Override public void run() { - multisigContractPropogated(providedContract, multisigContract.getHash()); + multisigContractPropogated(providedContract, contract.getHash()); } }, Threading.SAME_THREAD); } @@ -413,9 +443,6 @@ public class PaymentChannelServer { checkState(connectionOpen); if (channelSettling) return; - // If we generate an error, we set errorBuilder and closeReason and break, otherwise we return - Protos.Error.Builder errorBuilder; - CloseReason closeReason; try { switch (msg.getType()) { case CLIENT_VERSION: @@ -532,18 +559,18 @@ public class PaymentChannelServer { connectionOpen = false; try { - if (state != null && state.getMultisigContract() != null) { + if (state != null && state.getContract() != null) { StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); if (channels != null) { - StoredServerChannel storedServerChannel = channels.getChannel(state.getMultisigContract().getHash()); + StoredServerChannel storedServerChannel = channels.getChannel(state.getContract().getHash()); if (storedServerChannel != null) { storedServerChannel.clearConnectedHandler(); } } } } catch (IllegalStateException e) { - // Expected when we call getMultisigContract() sometimes + // Expected when we call getContract() sometimes } } finally { lock.unlock(); diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerState.java index 34e1b3a57..f1200d4e3 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerState.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerState.java @@ -16,24 +16,27 @@ 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 com.google.common.collect.ImmutableList; 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 com.google.common.util.concurrent.SettableFuture; +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; -import java.util.Arrays; -import java.util.Locale; -import static com.google.common.base.Preconditions.*; +import java.util.Arrays; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; /** *

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.

*/ -public class PaymentChannelServerState { +public abstract class PaymentChannelServerState { private static final Logger log = LoggerFactory.getLogger(PaymentChannelServerState.class); /** @@ -73,6 +79,7 @@ public class PaymentChannelServerState { * until READY, at which time the client can increase payment incrementally. */ public enum State { + UNINITIALISED, WAITING_FOR_REFUND_TRANSACTION, WAITING_FOR_MULTISIG_CONTRACT, WAITING_FOR_MULTISIG_ACCEPTANCE, @@ -81,55 +88,43 @@ public class PaymentChannelServerState { CLOSED, ERROR, } - private State state; - // The client and server keys for the multi-sig contract - // We currently also use the serverKey for payouts, but this is not required - private ECKey clientKey, serverKey; + protected StateMachine stateMachine; // Package-local for checkArguments in StoredServerChannel final Wallet wallet; // The object that will broadcast transactions for us - usually a peer group. - private final TransactionBroadcaster broadcaster; - - // The multi-sig contract and the output script from it - private Transaction multisigContract = null; - private Script multisigScript; + protected final TransactionBroadcaster broadcaster; // The last signature the client provided for a payment transaction. - private byte[] bestValueSignature; + protected byte[] bestValueSignature; - // The total value locked into the multi-sig output and the value to us in the last signature the client provided - private Coin totalValue; - private Coin bestValueToMe = Coin.ZERO; - private Coin feePaidForPayment; + protected Coin bestValueToMe = Coin.ZERO; - // The refund/change transaction output that goes back to the client - private TransactionOutput clientOutput; - private long refundTransactionUnlockTimeSecs; + // The server key for the multi-sig contract + // We currently also use the serverKey for payouts, but this is not required + protected ECKey serverKey; - private long minExpireTime; + protected long minExpireTime; - private StoredServerChannel storedServerChannel = null; + protected StoredServerChannel storedServerChannel = null; + + // The contract and the output script from it + protected Transaction contract = null; PaymentChannelServerState(StoredServerChannel storedServerChannel, Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException { synchronized (storedServerChannel) { + this.stateMachine = new StateMachine(State.UNINITIALISED, getStateTransitions()); this.wallet = checkNotNull(wallet); this.broadcaster = checkNotNull(broadcaster); - this.multisigContract = checkNotNull(storedServerChannel.contract); - this.multisigScript = multisigContract.getOutput(0).getScriptPubKey(); - this.clientKey = ECKey.fromPublicOnly(multisigScript.getChunks().get(1).data); - this.clientOutput = checkNotNull(storedServerChannel.clientOutput); - this.refundTransactionUnlockTimeSecs = storedServerChannel.refundTransactionUnlockTimeSecs; + this.contract = checkNotNull(storedServerChannel.contract); this.serverKey = checkNotNull(storedServerChannel.myKey); - this.totalValue = multisigContract.getOutput(0).getValue(); + this.storedServerChannel = storedServerChannel; this.bestValueToMe = checkNotNull(storedServerChannel.bestValueToMe); this.bestValueSignature = storedServerChannel.bestValueSignature; checkArgument(bestValueToMe.equals(Coin.ZERO) || bestValueSignature != null); - this.storedServerChannel = storedServerChannel; storedServerChannel.state = this; - this.state = State.READY; } } @@ -143,119 +138,75 @@ public class PaymentChannelServerState { * @param minExpireTime The earliest time at which the client can claim the refund transaction (UNIX timestamp of block) */ public PaymentChannelServerState(TransactionBroadcaster broadcaster, Wallet wallet, ECKey serverKey, long minExpireTime) { - this.state = State.WAITING_FOR_REFUND_TRANSACTION; + this.stateMachine = new StateMachine(State.UNINITIALISED, getStateTransitions()); this.serverKey = checkNotNull(serverKey); this.wallet = checkNotNull(wallet); this.broadcaster = checkNotNull(broadcaster); this.minExpireTime = minExpireTime; } - /** - * This object implements a state machine, and this accessor returns which state it's currently in. - */ + public abstract int getMajorVersion(); + public synchronized State getState() { - return state; + return stateMachine.getState(); } - /** - * Called when the client provides the refund transaction. - * The refund transaction must have one input from the multisig contract (that we don't have yet) and one output - * that the client creates to themselves. This object will later be modified when we start getting paid. - * - * @param refundTx The refund transaction, this object will be mutated when payment is incremented. - * @param clientMultiSigPubKey The client's pubkey which is required for the multisig output - * @return Our signature that makes the refund transaction valid - * @throws VerificationException If the transaction isnt valid or did not meet the requirements of a refund transaction. - */ - public synchronized byte[] provideRefundTransaction(Transaction refundTx, byte[] clientMultiSigPubKey) throws VerificationException { - checkNotNull(refundTx); - checkNotNull(clientMultiSigPubKey); - checkState(state == State.WAITING_FOR_REFUND_TRANSACTION); - log.info("Provided with refund transaction: {}", refundTx); - // Do a few very basic syntax sanity checks. - refundTx.verify(); - // Verify that the refund transaction has a single input (that we can fill to sign the multisig output). - if (refundTx.getInputs().size() != 1) - throw new VerificationException("Refund transaction does not have exactly one input"); - // Verify that the refund transaction has a time lock on it and a sequence number of zero. - if (refundTx.getInput(0).getSequenceNumber() != 0) - throw new VerificationException("Refund transaction's input's sequence number is non-0"); - if (refundTx.getLockTime() < minExpireTime) - throw new VerificationException("Refund transaction has a lock time too soon"); - // Verify the transaction has one output (we don't care about its contents, its up to the client) - // Note that because we sign with SIGHASH_NONE|SIGHASH_ANYOENCANPAY the client can later add more outputs and - // inputs, but we will need only one output later to create the paying transactions - if (refundTx.getOutputs().size() != 1) - throw new VerificationException("Refund transaction does not have exactly one output"); - - refundTransactionUnlockTimeSecs = refundTx.getLockTime(); - - // Sign the refund tx with the scriptPubKey and return the signature. We don't have the spending transaction - // so do the steps individually. - clientKey = ECKey.fromPublicOnly(clientMultiSigPubKey); - Script multisigPubKey = ScriptBuilder.createMultiSigOutputScript(2, ImmutableList.of(clientKey, serverKey)); - // We are really only signing the fact that the transaction has a proper lock time and don't care about anything - // else, so we sign SIGHASH_NONE and SIGHASH_ANYONECANPAY. - TransactionSignature sig = refundTx.calculateSignature(0, serverKey, multisigPubKey, Transaction.SigHash.NONE, true); - log.info("Signed refund transaction."); - this.clientOutput = refundTx.getOutput(0); - state = State.WAITING_FOR_MULTISIG_CONTRACT; - return sig.encodeToBitcoin(); - } + protected abstract Multimap getStateTransitions(); /** * Called when the client provides the multi-sig contract. Checks that the previously-provided refund transaction * spends this transaction (because we will use it as a base to create payment transactions) as well as output value * and form (ie it is a 2-of-2 multisig to the correct keys). * - * @param multisigContract The provided multisig contract. Do not mutate this object after this call. + * @param contract The provided multisig contract. Do not mutate this object after this call. * @return A future which completes when the provided multisig contract successfully broadcasts, or throws if the broadcast fails for some reason * Note that if the network simply rejects the transaction, this future will never complete, a timeout should be used. * @throws VerificationException If the provided multisig contract is not well-formed or does not meet previously-specified parameters */ - public synchronized ListenableFuture provideMultiSigContract(final Transaction multisigContract) throws VerificationException { - checkNotNull(multisigContract); - checkState(state == State.WAITING_FOR_MULTISIG_CONTRACT); + public synchronized ListenableFuture provideContract(final Transaction contract) throws VerificationException { + checkNotNull(contract); + stateMachine.checkState(State.WAITING_FOR_MULTISIG_CONTRACT); try { - multisigContract.verify(); - this.multisigContract = multisigContract; - this.multisigScript = multisigContract.getOutput(0).getScriptPubKey(); + contract.verify(); + this.contract = contract; + verifyContract(contract); - // Check that multisigContract's first output is a 2-of-2 multisig to the correct pubkeys in the correct order - final Script expectedScript = ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(clientKey, serverKey)); - if (!Arrays.equals(multisigScript.getProgram(), expectedScript.getProgram())) - throw new VerificationException("Multisig contract's first output was not a standard 2-of-2 multisig to client and server in that order."); + // Check that contract's first output is a 2-of-2 multisig to the correct pubkeys in the correct order + final Script expectedScript = createOutputScript(); + if (!Arrays.equals(getContractScript().getProgram(), expectedScript.getProgram())) + throw new VerificationException(getMajorVersion() == 1 ? + "Contract's first output was not a standard 2-of-2 multisig to client and server in that order." : + "Contract was not a P2SH script of a CLTV redeem script to client and server"); - this.totalValue = multisigContract.getOutput(0).getValue(); - if (this.totalValue.signum() <= 0) + if (getTotalValue().signum() <= 0) throw new VerificationException("Not accepting an attempt to open a contract with zero value."); } catch (VerificationException e) { // We couldn't parse the multisig transaction or its output. - log.error("Provided multisig contract did not verify: {}", multisigContract.toString()); + log.error("Provided multisig contract did not verify: {}", contract.toString()); throw e; } - log.info("Broadcasting multisig contract: {}", multisigContract); - wallet.addWatchedScripts(ImmutableList.of(multisigContract.getOutput(0).getScriptPubKey())); - state = State.WAITING_FOR_MULTISIG_ACCEPTANCE; + log.info("Broadcasting multisig contract: {}", contract); + wallet.addWatchedScripts(ImmutableList.of(contract.getOutput(0).getScriptPubKey())); + stateMachine.transition(State.WAITING_FOR_MULTISIG_ACCEPTANCE); final SettableFuture future = SettableFuture.create(); - Futures.addCallback(broadcaster.broadcastTransaction(multisigContract).future(), new FutureCallback() { + Futures.addCallback(broadcaster.broadcastTransaction(contract).future(), new FutureCallback() { @Override public void onSuccess(Transaction transaction) { log.info("Successfully broadcast multisig contract {}. Channel now open.", transaction.getHashAsString()); try { - // Manually add the multisigContract to the wallet, overriding the isRelevant checks so we can track + // Manually add the contract to the wallet, overriding the isRelevant checks so we can track // it and check for double-spends later - wallet.receivePending(multisigContract, null, true); + wallet.receivePending(contract, null, true); } catch (VerificationException e) { - throw new RuntimeException(e); // Cannot happen, we already called multisigContract.verify() + throw new RuntimeException(e); // Cannot happen, we already called contract.verify() } - state = State.READY; + stateMachine.transition(State.READY); future.set(PaymentChannelServerState.this); } @Override public void onFailure(Throwable throwable) { // Couldn't broadcast the transaction for some reason. - log.error("Broadcast multisig contract failed", throwable); - state = State.ERROR; + log.error("Failed to broadcast contract", throwable); + stateMachine.transition(State.ERROR); future.setException(throwable); } }); @@ -263,18 +214,17 @@ public class PaymentChannelServerState { } // Create a payment transaction with valueToMe going back to us - private synchronized Wallet.SendRequest makeUnsignedChannelContract(Coin valueToMe) { + protected synchronized Wallet.SendRequest makeUnsignedChannelContract(Coin valueToMe) { Transaction tx = new Transaction(wallet.getParams()); - if (!totalValue.subtract(valueToMe).equals(Coin.ZERO)) { - clientOutput.setValue(totalValue.subtract(valueToMe)); - tx.addOutput(clientOutput); + if (!getTotalValue().subtract(valueToMe).equals(Coin.ZERO)) { + tx.addOutput(getTotalValue().subtract(valueToMe), getClientKey().toAddress(wallet.getParams())); } - tx.addInput(multisigContract.getOutput(0)); + tx.addInput(contract.getOutput(0)); return Wallet.SendRequest.forTx(tx); } /** - * Called when the client provides us with a new signature and wishes to increment total payment by size. + * Called when the client provides us with a new signature and wishes to increment total payment by size. + * Verifies the provided signature and only updates values if everything checks out. * If the new refundSize is not the lowest we have seen, it is simply ignored. * @@ -284,26 +234,29 @@ public class PaymentChannelServerState { * @return true if there is more value left on the channel, false if it is now fully used up. */ public synchronized boolean incrementPayment(Coin refundSize, byte[] signatureBytes) throws VerificationException, ValueOutOfRangeException, InsufficientMoneyException { - checkState(state == State.READY); + stateMachine.checkState(State.READY); checkNotNull(refundSize); checkNotNull(signatureBytes); TransactionSignature signature = TransactionSignature.decodeFromBitcoin(signatureBytes, true); // We allow snapping to zero for the payment amount because it's treated specially later, but not less than // the dust level because that would prevent the transaction from being relayed/mined. final boolean fullyUsedUp = refundSize.equals(Coin.ZERO); - if (refundSize.compareTo(clientOutput.getMinNonDustValue()) < 0 && !fullyUsedUp) - throw new ValueOutOfRangeException("Attempt to refund negative value or value too small to be accepted by the network"); - Coin newValueToMe = totalValue.subtract(refundSize); + Coin newValueToMe = getTotalValue().subtract(refundSize); if (newValueToMe.signum() < 0) throw new ValueOutOfRangeException("Attempt to refund more than the contract allows."); if (newValueToMe.compareTo(bestValueToMe) < 0) throw new ValueOutOfRangeException("Attempt to roll back payment on the channel."); - // Get the wallet's copy of the multisigContract (ie with confidence information), if this is null, the wallet + Wallet.SendRequest req = makeUnsignedChannelContract(newValueToMe); + + if (!fullyUsedUp && refundSize.compareTo(req.tx.getOutput(0).getMinNonDustValue()) < 0) + throw new ValueOutOfRangeException("Attempt to refund negative value or value too small to be accepted by the network"); + + // Get the wallet's copy of the contract (ie with confidence information), if this is null, the wallet // was not connected to the peergroup when the contract was broadcast (which may cause issues down the road, and // disables our double-spend check next) - Transaction walletContract = wallet.getTransaction(multisigContract.getHash()); - checkNotNull(walletContract, "Wallet did not contain multisig contract {} after state was marked READY", multisigContract.getHash()); + Transaction walletContract = wallet.getTransaction(contract.getHash()); + checkNotNull(walletContract, "Wallet did not contain multisig contract {} after state was marked READY", contract.getHash()); // Note that we check for DEAD state here, but this test is essentially useless in production because we will // miss most double-spends due to bloom filtering right now anyway. This will eventually fixed by network-wide @@ -324,13 +277,12 @@ public class PaymentChannelServerState { if (signature.sigHashMode() != mode || !signature.anyoneCanPay()) throw new VerificationException("New payment signature was not signed with the right SIGHASH flags."); - Wallet.SendRequest req = makeUnsignedChannelContract(newValueToMe); // Now check the signature is correct. // Note that the client must sign with SIGHASH_{SINGLE/NONE} | SIGHASH_ANYONECANPAY to allow us to add additional // inputs (in case we need to add significant fee, or something...) and any outputs we want to pay to. - Sha256Hash sighash = req.tx.hashForSignature(0, multisigScript, mode, true); + Sha256Hash sighash = req.tx.hashForSignature(0, getSignedScript(), mode, true); - if (!clientKey.verify(sighash, signature)) + if (!getClientKey().verify(sighash, signature)) throw new VerificationException("Signature does not verify on tx\n" + req.tx); bestValueToMe = newValueToMe; bestValueSignature = signatureBytes; @@ -338,103 +290,15 @@ public class PaymentChannelServerState { return !fullyUsedUp; } - // Signs the first input of the transaction which must spend the multisig contract. - private void signMultisigInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) { - TransactionSignature signature = tx.calculateSignature(0, serverKey, multisigScript, hashType, anyoneCanPay); - byte[] mySig = signature.encodeToBitcoin(); - Script scriptSig = ScriptBuilder.createMultiSigInputScriptBytes(ImmutableList.of(bestValueSignature, mySig)); - tx.getInput(0).setScriptSig(scriptSig); - } - - final SettableFuture closedFuture = SettableFuture.create(); /** *

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. - *

- * * @return a future which completes when the provided multisig contract successfully broadcasts, or throws if the * broadcast fails for some reason. Note that if the network simply rejects the transaction, this future * will never complete, a timeout should be used. * @throws InsufficientMoneyException If the payment tx would have cost more in fees to spend than it is worth. */ - public synchronized ListenableFuture close() throws InsufficientMoneyException { - if (storedServerChannel != null) { - StoredServerChannel temp = storedServerChannel; - storedServerChannel = null; - StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) - wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); - channels.closeChannel(temp); // May call this method again for us (if it wasn't the original caller) - if (state.compareTo(State.CLOSING) >= 0) - return closedFuture; - } - - if (state.ordinal() < State.READY.ordinal()) { - log.error("Attempt to settle channel in state " + state); - state = State.CLOSED; - closedFuture.set(null); - return closedFuture; - } - if (state != State.READY) { - // TODO: What is this codepath for? - log.warn("Failed attempt to settle a channel in state " + state); - return closedFuture; - } - Transaction tx = null; - try { - Wallet.SendRequest req = makeUnsignedChannelContract(bestValueToMe); - tx = req.tx; - // Provide a throwaway signature so that completeTx won't complain out about unsigned inputs it doesn't - // know how to sign. Note that this signature does actually have to be valid, so we can't use a dummy - // signature to save time, because otherwise completeTx will try to re-sign it to make it valid and then - // die. We could probably add features to the SendRequest API to make this a bit more efficient. - signMultisigInput(tx, Transaction.SigHash.NONE, true); - // Let wallet handle adding additional inputs/fee as necessary. - req.shuffleOutputs = false; - req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG; - wallet.completeTx(req); // TODO: Fix things so shuffling is usable. - feePaidForPayment = req.tx.getFee(); - log.info("Calculated fee is {}", feePaidForPayment); - if (feePaidForPayment.compareTo(bestValueToMe) > 0) { - final String msg = String.format(Locale.US, "Had to pay more in fees (%s) than the channel was worth (%s)", - feePaidForPayment, bestValueToMe); - throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg); - } - // Now really sign the multisig input. - signMultisigInput(tx, Transaction.SigHash.ALL, false); - // Some checks that shouldn't be necessary but it can't hurt to check. - tx.verify(); // Sanity check syntax. - for (TransactionInput input : tx.getInputs()) - input.verify(); // Run scripts and ensure it is valid. - } catch (InsufficientMoneyException e) { - throw e; // Don't fall through. - } catch (Exception e) { - log.error("Could not verify self-built tx\nMULTISIG {}\nCLOSE {}", multisigContract, tx != null ? tx : ""); - throw new RuntimeException(e); // Should never happen. - } - state = State.CLOSING; - log.info("Closing channel, broadcasting tx {}", tx); - // The act of broadcasting the transaction will add it to the wallet. - ListenableFuture future = broadcaster.broadcastTransaction(tx).future(); - Futures.addCallback(future, new FutureCallback() { - @Override public void onSuccess(Transaction transaction) { - log.info("TX {} propagated, channel successfully closed.", transaction.getHash()); - state = State.CLOSED; - closedFuture.set(transaction); - } - - @Override public void onFailure(Throwable throwable) { - log.error("Failed to settle channel, could not broadcast", throwable); - state = State.ERROR; - closedFuture.setException(throwable); - } - }); - return closedFuture; - } + public abstract ListenableFuture close() throws InsufficientMoneyException; /** * Gets the highest payment to ourselves (which we will receive on settle(), not including fees) @@ -446,29 +310,21 @@ public class PaymentChannelServerState { /** * Gets the fee paid in the final payment transaction (only available if settle() did not throw an exception) */ - public synchronized Coin getFeePaid() { - checkState(state == State.CLOSED || state == State.CLOSING); - return feePaidForPayment; - } + public abstract Coin getFeePaid(); /** * Gets the multisig contract which was used to initialize this channel */ - public synchronized Transaction getMultisigContract() { - checkState(multisigContract != null); - return multisigContract; + public synchronized Transaction getContract() { + checkState(contract != null); + return contract; } - /** - * Gets the client's refund transaction which they can spend to get the entire channel value back if it reaches its - * lock time. - */ - public synchronized long getRefundTransactionUnlockTime() { - checkState(state.compareTo(State.WAITING_FOR_MULTISIG_CONTRACT) > 0 && state != State.ERROR); - return refundTransactionUnlockTimeSecs; + public long getExpiryTime() { + return minExpireTime; } - private synchronized void updateChannelInWallet() { + protected synchronized void updateChannelInWallet() { if (storedServerChannel != null) { storedServerChannel.updateValueToMe(bestValueToMe, bestValueSignature); StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) @@ -480,7 +336,7 @@ public class PaymentChannelServerState { /** * Stores this channel's state in the wallet as a part of a {@link StoredPaymentChannelServerStates} wallet * extension and keeps it up-to-date each time payment is incremented. This will be automatically removed when - * a call to {@link PaymentChannelServerState#close()} completes successfully. A channel may only be stored after it + * a call to {@link PaymentChannelV1ServerState#close()} completes successfully. A channel may only be stored after it * has fully opened (ie state == State.READY). * * @param connectedHandler Optional {@link PaymentChannelServer} object that manages this object. This will @@ -489,16 +345,47 @@ public class PaymentChannelServerState { * handler which can then do a TCP disconnect. */ public synchronized void storeChannelInWallet(@Nullable PaymentChannelServer connectedHandler) { - checkState(state == State.READY); + stateMachine.checkState(State.READY); if (storedServerChannel != null) return; - log.info("Storing state with contract hash {}.", multisigContract.getHash()); + log.info("Storing state with contract hash {}.", getContract().getHash()); StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) wallet.addOrGetExistingExtension(new StoredPaymentChannelServerStates(wallet, broadcaster)); - storedServerChannel = new StoredServerChannel(this, multisigContract, clientOutput, refundTransactionUnlockTimeSecs, serverKey, bestValueToMe, bestValueSignature); + storedServerChannel = new StoredServerChannel(this, getMajorVersion(), getContract(), getClientOutput(), getExpiryTime(), serverKey, getClientKey(), bestValueToMe, bestValueSignature); if (connectedHandler != null) checkState(storedServerChannel.setConnectedHandler(connectedHandler, false) == connectedHandler); channels.putChannel(storedServerChannel); } + + public abstract TransactionOutput getClientOutput(); + + public Script getContractScript() { + if (contract == null) { + return null; + } + return contract.getOutput(0).getScriptPubKey(); + } + + /** + * Gets the script that signatures should sign against. This is never a P2SH + * script, rather the script that would be inside a P2SH script. + * @return + */ + protected abstract Script getSignedScript(); + + /** + * Verifies that the given contract meets a set of extra requirements + * @param contract + */ + protected void verifyContract(final Transaction contract) { + } + + protected abstract Script createOutputScript(); + + protected Coin getTotalValue() { + return contract.getOutput(0).getValue(); + } + + protected abstract ECKey getClientKey(); } diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ClientState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ClientState.java new file mode 100644 index 000000000..7ec56a77e --- /dev/null +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ClientState.java @@ -0,0 +1,297 @@ +/* + * 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.Multimap; +import com.google.common.collect.MultimapBuilder; +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.wallet.AllowUnconfirmedCoinSelector; +import org.spongycastle.crypto.params.KeyParameter; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.List; + +import static com.google.common.base.Preconditions.*; + +/** + * Version 1 of the payment channel state machine - uses time locked multisig + * contracts. + */ +public class PaymentChannelV1ClientState extends PaymentChannelClientState { + private static final Logger log = LoggerFactory.getLogger(PaymentChannelV1ClientState.class); + // 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; + + PaymentChannelV1ClientState(StoredClientChannel storedClientChannel, Wallet wallet) throws VerificationException { + super(storedClientChannel, wallet); + // The PaymentChannelClientConnection handles storedClientChannel.active and ensures we aren't resuming channels + this.multisigContract = checkNotNull(storedClientChannel.contract); + this.multisigScript = multisigContract.getOutput(0).getScriptPubKey(); + this.refundTx = checkNotNull(storedClientChannel.refund); + this.refundFees = checkNotNull(storedClientChannel.refundFees); + this.expiryTime = refundTx.getLockTime(); + this.totalValue = multisigContract.getOutput(0).getValue(); + stateMachine.transition(State.READY); + initWalletListeners(); + } + + /** + * Creates a state object for a payment channel client. It is expected that you be ready to + * {@link PaymentChannelV1ClientState#initiate()} after construction (to avoid creating objects for channels which are + * not going to finish opening) and thus some parameters provided here are only used in + * {@link PaymentChannelV1ClientState#initiate()} to create the Multisig contract and refund transaction. + * + * @param wallet a wallet that contains at least the specified amount of value. + * @param myKey a freshly generated private key for this channel. + * @param serverMultisigKey a public key retrieved from the server used for the initial multisig contract + * @param value how many satoshis to put into this contract. If the channel reaches this limit, it must be closed. + * It is suggested you use at least {@link Coin#CENT} to avoid paying fees if you need to spend the refund transaction + * @param expiryTimeInSeconds At what point (UNIX timestamp +/- a few hours) the channel will expire + * + * @throws VerificationException If either myKey's pubkey or serverKey's pubkey are non-canonical (ie invalid) + */ + public PaymentChannelV1ClientState(Wallet wallet, ECKey myKey, ECKey serverMultisigKey, + Coin value, long expiryTimeInSeconds) throws VerificationException { + super(wallet, myKey, serverMultisigKey, value, expiryTimeInSeconds); + checkArgument(value.signum() > 0); + initWalletListeners(); + this.totalValue = checkNotNull(value); + this.expiryTime = expiryTimeInSeconds; + stateMachine.transition(State.NEW); + } + + @Override + protected Multimap getStateTransitions() { + Multimap result = MultimapBuilder.enumKeys(State.class).arrayListValues().build(); + result.put(State.UNINITIALISED, State.NEW); + result.put(State.UNINITIALISED, State.READY); + result.put(State.NEW, State.INITIATED); + result.put(State.INITIATED, State.WAITING_FOR_SIGNED_REFUND); + result.put(State.WAITING_FOR_SIGNED_REFUND, State.SAVE_STATE_IN_WALLET); + result.put(State.SAVE_STATE_IN_WALLET, State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER); + result.put(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, State.READY); + result.put(State.READY, State.EXPIRED); + result.put(State.READY, State.CLOSED); + return result; + } + + public int getMajorVersion() { + return 1; + } + + /** + * Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate + * time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and + * {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by + * overriding {@link PaymentChannelV1ClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}. + * By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low. + * @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted. + * The wallet KeyCrypter is assumed. + * + * @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network + * @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate + */ + @Override + public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException { + final NetworkParameters params = wallet.getParams(); + Transaction template = new Transaction(params); + // We always place the client key before the server key because, if either side wants some privacy, they can + // use a fresh key for the the multisig contract and nowhere else + List keys = Lists.newArrayList(myKey, serverKey); + // There is also probably a change output, but we don't bother shuffling them as it's obvious from the + // format which one is the change. If we start obfuscating the change output better in future this may + // be worth revisiting. + TransactionOutput multisigOutput = template.addOutput(totalValue, ScriptBuilder.createMultiSigOutputScript(2, keys)); + if (multisigOutput.getMinNonDustValue().compareTo(totalValue) > 0) + throw new ValueOutOfRangeException("totalValue too small to use"); + Wallet.SendRequest req = Wallet.SendRequest.forTx(template); + req.coinSelector = AllowUnconfirmedCoinSelector.get(); + editContractSendRequest(req); + req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable. + req.aesKey = userKey; + wallet.completeTx(req); + Coin multisigFee = req.tx.getFee(); + multisigContract = req.tx; + // Build a refund transaction that protects us in the case of a bad server that's just trying to cause havoc + // by locking up peoples money (perhaps as a precursor to a ransom attempt). We time lock it so the server + // has an assurance that we cannot take back our money by claiming a refund before the channel closes - this + // relies on the fact that since Bitcoin 0.8 time locked transactions are non-final. This will need to change + // in future as it breaks the intended design of timelocking/tx replacement, but for now it simplifies this + // specific protocol somewhat. + refundTx = new Transaction(params); + refundTx.addInput(multisigOutput).setSequenceNumber(0); // Allow replacement when it's eventually reactivated. + refundTx.setLockTime(expiryTime); + if (totalValue.compareTo(Coin.CENT) < 0) { + // Must pay min fee. + final Coin valueAfterFee = totalValue.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + if (Transaction.MIN_NONDUST_OUTPUT.compareTo(valueAfterFee) > 0) + throw new ValueOutOfRangeException("totalValue too small to use"); + refundTx.addOutput(valueAfterFee, myKey.toAddress(params)); + refundFees = multisigFee.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + } else { + refundTx.addOutput(totalValue, myKey.toAddress(params)); + refundFees = multisigFee; + } + refundTx.getConfidence().setSource(TransactionConfidence.Source.SELF); + log.info("initiated channel with multi-sig contract {}, refund {}", multisigContract.getHashAsString(), + refundTx.getHashAsString()); + stateMachine.transition(State.INITIATED); + // Client should now call getIncompleteRefundTransaction() and send it to the server. + } + + /** + * Returns the transaction that locks the money to the agreement of both parties. Do not mutate the result. + * Once this step is done, you can use {@link PaymentChannelClientState#incrementPaymentBy(Coin, KeyParameter)} to + * start paying the server. + */ + @Override + public synchronized Transaction getContract() { + checkState(multisigContract != null); + if (stateMachine.getState() == State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER) { + stateMachine.transition(State.READY); + } + return multisigContract; + } + + @Override + protected synchronized Transaction getContractInternal() { + return multisigContract; + } + + protected synchronized Script getContractScript() { + return multisigScript; + } + + @Override + protected Script getSignedScript() { + return getContractScript(); + } + + /** + * Returns a partially signed (invalid) refund transaction that should be passed to the server. Once the server + * has checked it out and provided its own signature, call + * {@link PaymentChannelV1ClientState#provideRefundSignature(byte[], KeyParameter)} with the result. + */ + public synchronized Transaction getIncompleteRefundTransaction() { + checkState(refundTx != null); + if (stateMachine.getState() == State.INITIATED) { + stateMachine.transition(State.WAITING_FOR_SIGNED_REFUND); + } + return refundTx; + } + + /** + *

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.

+ */ + public synchronized void provideRefundSignature(byte[] theirSignature, @Nullable KeyParameter userKey) + throws VerificationException { + checkNotNull(theirSignature); + stateMachine.checkState(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); + stateMachine.transition(State.SAVE_STATE_IN_WALLET); + } + + @Override + protected synchronized Coin getValueToMe() { + return valueToMe; + } + + protected long getExpiryTime() { + return expiryTime; + } + + @Override + @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(getMajorVersion(), id, multisigContract, refundTx, myKey, serverKey, valueToMe, refundFees, 0, true); + channels.putChannel(storedChannel); + } + + @Override + public synchronized Coin getRefundTxFees() { + checkState(getState().compareTo(State.NEW) > 0); + return refundFees; + } + + @VisibleForTesting Transaction getRefundTransaction() { + return refundTx; + } + + /** + * Once the servers signature over the refund transaction has been received and provided using + * {@link PaymentChannelV1ClientState#provideRefundSignature(byte[], KeyParameter)} then this + * method can be called to receive the now valid and broadcastable refund transaction. + */ + public synchronized Transaction getCompletedRefundTransaction() { + checkState(getState().compareTo(State.WAITING_FOR_SIGNED_REFUND) > 0); + return refundTx; + } + + /** + * Gets the total value of this channel (ie the maximum payment possible) + */ + @Override + public Coin getTotalValue() { + return totalValue; + } +} diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ServerState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ServerState.java new file mode 100644 index 000000000..aad85bf99 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ServerState.java @@ -0,0 +1,279 @@ +/* + * 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.*; +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Locale; + +import static com.google.common.base.Preconditions.*; + +/** + * Version 1 of the payment channel server state object. Common functionality is + * present in the parent class. + */ +public class PaymentChannelV1ServerState extends PaymentChannelServerState { + private static final Logger log = LoggerFactory.getLogger(PaymentChannelV1ServerState.class); + + // The total value locked into the multi-sig output and the value to us in the last signature the client provided + private Coin feePaidForPayment; + + // The client key for the multi-sig contract + // We currently also use the serverKey for payouts, but this is not required + protected ECKey clientKey; + + // The refund/change transaction output that goes back to the client + private TransactionOutput clientOutput; + private long refundTransactionUnlockTimeSecs; + + PaymentChannelV1ServerState(StoredServerChannel storedServerChannel, Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException { + super(storedServerChannel, wallet, broadcaster); + synchronized (storedServerChannel) { + this.clientKey = ECKey.fromPublicOnly(getContractScript().getChunks().get(1).data); + this.clientOutput = checkNotNull(storedServerChannel.clientOutput); + this.refundTransactionUnlockTimeSecs = storedServerChannel.refundTransactionUnlockTimeSecs; + stateMachine.transition(State.READY); + } + } + + /** + * Creates a new state object to track the server side of a payment channel. + * + * @param broadcaster The peer group which we will broadcast transactions to, this should have multiple peers + * @param wallet The wallet which will be used to complete transactions + * @param serverKey The private key which we use for our part of the multi-sig contract + * (this MUST be fresh and CANNOT be used elsewhere) + * @param minExpireTime The earliest time at which the client can claim the refund transaction (UNIX timestamp of block) + */ + public PaymentChannelV1ServerState(TransactionBroadcaster broadcaster, Wallet wallet, ECKey serverKey, long minExpireTime) { + super(broadcaster, wallet, serverKey, minExpireTime); + stateMachine.transition(State.WAITING_FOR_REFUND_TRANSACTION); + } + + @Override + public Multimap getStateTransitions() { + Multimap result = MultimapBuilder.enumKeys(State.class).arrayListValues().build(); + result.put(State.UNINITIALISED, State.READY); + result.put(State.UNINITIALISED, State.WAITING_FOR_REFUND_TRANSACTION); + result.put(State.WAITING_FOR_REFUND_TRANSACTION, State.WAITING_FOR_MULTISIG_CONTRACT); + result.put(State.WAITING_FOR_MULTISIG_CONTRACT, State.WAITING_FOR_MULTISIG_ACCEPTANCE); + result.put(State.WAITING_FOR_MULTISIG_ACCEPTANCE, State.READY); + result.put(State.READY, State.CLOSING); + result.put(State.CLOSING, State.CLOSED); + for (State state : State.values()) { + result.put(state, State.ERROR); + } + return result; + } + + @Override + public int getMajorVersion() { + return 1; + } + + @Override + public TransactionOutput getClientOutput() { + return clientOutput; + } + + @Override + protected Script getSignedScript() { + return getContractScript(); + } + + /** + * Called when the client provides the refund transaction. + * The refund transaction must have one input from the multisig contract (that we don't have yet) and one output + * that the client creates to themselves. This object will later be modified when we start getting paid. + * + * @param refundTx The refund transaction, this object will be mutated when payment is incremented. + * @param clientMultiSigPubKey The client's pubkey which is required for the multisig output + * @return Our signature that makes the refund transaction valid + * @throws VerificationException If the transaction isnt valid or did not meet the requirements of a refund transaction. + */ + public synchronized byte[] provideRefundTransaction(Transaction refundTx, byte[] clientMultiSigPubKey) throws VerificationException { + checkNotNull(refundTx); + checkNotNull(clientMultiSigPubKey); + stateMachine.checkState(State.WAITING_FOR_REFUND_TRANSACTION); + log.info("Provided with refund transaction: {}", refundTx); + // Do a few very basic syntax sanity checks. + refundTx.verify(); + // Verify that the refund transaction has a single input (that we can fill to sign the multisig output). + if (refundTx.getInputs().size() != 1) + throw new VerificationException("Refund transaction does not have exactly one input"); + // Verify that the refund transaction has a time lock on it and a sequence number of zero. + if (refundTx.getInput(0).getSequenceNumber() != 0) + throw new VerificationException("Refund transaction's input's sequence number is non-0"); + if (refundTx.getLockTime() < minExpireTime) + throw new VerificationException("Refund transaction has a lock time too soon"); + // Verify the transaction has one output (we don't care about its contents, its up to the client) + // Note that because we sign with SIGHASH_NONE|SIGHASH_ANYOENCANPAY the client can later add more outputs and + // inputs, but we will need only one output later to create the paying transactions + if (refundTx.getOutputs().size() != 1) + throw new VerificationException("Refund transaction does not have exactly one output"); + + refundTransactionUnlockTimeSecs = refundTx.getLockTime(); + + // Sign the refund tx with the scriptPubKey and return the signature. We don't have the spending transaction + // so do the steps individually. + clientKey = ECKey.fromPublicOnly(clientMultiSigPubKey); + Script multisigPubKey = ScriptBuilder.createMultiSigOutputScript(2, ImmutableList.of(clientKey, serverKey)); + // We are really only signing the fact that the transaction has a proper lock time and don't care about anything + // else, so we sign SIGHASH_NONE and SIGHASH_ANYONECANPAY. + TransactionSignature sig = refundTx.calculateSignature(0, serverKey, multisigPubKey, Transaction.SigHash.NONE, true); + log.info("Signed refund transaction."); + this.clientOutput = refundTx.getOutput(0); + stateMachine.transition(State.WAITING_FOR_MULTISIG_CONTRACT); + return sig.encodeToBitcoin(); + } + + protected Script createOutputScript() { + return ScriptBuilder.createMultiSigOutputScript(2, ImmutableList.of(clientKey, serverKey)); + } + + protected ECKey getClientKey() { + return clientKey; + } + + // Signs the first input of the transaction which must spend the multisig contract. + private void signMultisigInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) { + TransactionSignature signature = tx.calculateSignature(0, serverKey, getContractScript(), hashType, anyoneCanPay); + byte[] mySig = signature.encodeToBitcoin(); + Script scriptSig = ScriptBuilder.createMultiSigInputScriptBytes(ImmutableList.of(bestValueSignature, mySig)); + tx.getInput(0).setScriptSig(scriptSig); + } + + final SettableFuture closedFuture = SettableFuture.create(); + /** + *

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. + *

+ * + * @return a future which completes when the provided multisig contract successfully broadcasts, or throws if the + * broadcast fails for some reason. Note that if the network simply rejects the transaction, this future + * will never complete, a timeout should be used. + * @throws InsufficientMoneyException If the payment tx would have cost more in fees to spend than it is worth. + */ + @Override + public synchronized ListenableFuture close() throws InsufficientMoneyException { + if (storedServerChannel != null) { + StoredServerChannel temp = storedServerChannel; + storedServerChannel = null; + StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) + wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); + channels.closeChannel(temp); // May call this method again for us (if it wasn't the original caller) + if (getState().compareTo(State.CLOSING) >= 0) + return closedFuture; + } + + if (getState().ordinal() < State.READY.ordinal()) { + log.error("Attempt to settle channel in state " + getState()); + stateMachine.transition(State.CLOSED); + closedFuture.set(null); + return closedFuture; + } + if (getState() != State.READY) { + // TODO: What is this codepath for? + log.warn("Failed attempt to settle a channel in state " + getState()); + return closedFuture; + } + Transaction tx = null; + try { + Wallet.SendRequest req = makeUnsignedChannelContract(bestValueToMe); + tx = req.tx; + // Provide a throwaway signature so that completeTx won't complain out about unsigned inputs it doesn't + // know how to sign. Note that this signature does actually have to be valid, so we can't use a dummy + // signature to save time, because otherwise completeTx will try to re-sign it to make it valid and then + // die. We could probably add features to the SendRequest API to make this a bit more efficient. + signMultisigInput(tx, Transaction.SigHash.NONE, true); + // Let wallet handle adding additional inputs/fee as necessary. + req.shuffleOutputs = false; + req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG; + wallet.completeTx(req); // TODO: Fix things so shuffling is usable. + feePaidForPayment = req.tx.getFee(); + log.info("Calculated fee is {}", feePaidForPayment); + if (feePaidForPayment.compareTo(bestValueToMe) > 0) { + final String msg = String.format(Locale.US, "Had to pay more in fees (%s) than the channel was worth (%s)", + feePaidForPayment, bestValueToMe); + throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg); + } + // Now really sign the multisig input. + signMultisigInput(tx, Transaction.SigHash.ALL, false); + // Some checks that shouldn't be necessary but it can't hurt to check. + tx.verify(); // Sanity check syntax. + for (TransactionInput input : tx.getInputs()) + input.verify(); // Run scripts and ensure it is valid. + } catch (InsufficientMoneyException e) { + throw e; // Don't fall through. + } catch (Exception e) { + log.error("Could not verify self-built tx\nMULTISIG {}\nCLOSE {}", contract, tx != null ? tx : ""); + throw new RuntimeException(e); // Should never happen. + } + stateMachine.transition(State.CLOSING); + log.info("Closing channel, broadcasting tx {}", tx); + // The act of broadcasting the transaction will add it to the wallet. + ListenableFuture future = broadcaster.broadcastTransaction(tx).future(); + Futures.addCallback(future, new FutureCallback() { + @Override public void onSuccess(Transaction transaction) { + log.info("TX {} propagated, channel successfully closed.", transaction.getHash()); + stateMachine.transition(State.CLOSED); + closedFuture.set(transaction); + } + + @Override public void onFailure(Throwable throwable) { + log.error("Failed to settle channel, could not broadcast: {}", throwable); + stateMachine.transition(State.ERROR); + closedFuture.setException(throwable); + } + }); + return closedFuture; + } + + /** + * Gets the fee paid in the final payment transaction (only available if settle() did not throw an exception) + */ + @Override + public synchronized Coin getFeePaid() { + stateMachine.checkState(State.CLOSED, State.CLOSING); + return feePaidForPayment; + } + + /** + * Gets the client's refund transaction which they can spend to get the entire channel value back if it reaches its + * lock time. + */ + public synchronized long getRefundTransactionUnlockTime() { + checkState(getState().compareTo(State.WAITING_FOR_MULTISIG_CONTRACT) > 0 && getState() != State.ERROR); + return refundTransactionUnlockTimeSecs; + } +} diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ClientState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ClientState.java new file mode 100644 index 000000000..63752b883 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ClientState.java @@ -0,0 +1,212 @@ +/* + * 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.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.wallet.AllowUnconfirmedCoinSelector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongycastle.crypto.params.KeyParameter; + +import javax.annotation.Nullable; +import java.math.BigInteger; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * Version 2 of the payment channel state machine - uses CLTV opcode transactions + * instead of multisig transactions. + */ +public class PaymentChannelV2ClientState extends PaymentChannelClientState { + private static final Logger log = LoggerFactory.getLogger(PaymentChannelV1ClientState.class); + + // 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. + // Unlike in V1 this refund isn't signed by the server - we only have to sign it ourselves. + @VisibleForTesting 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 contract; + + PaymentChannelV2ClientState(StoredClientChannel storedClientChannel, Wallet wallet) throws VerificationException { + super(storedClientChannel, wallet); + // The PaymentChannelClientConnection handles storedClientChannel.active and ensures we aren't resuming channels + this.contract = checkNotNull(storedClientChannel.contract); + this.expiryTime = storedClientChannel.expiryTime; + this.totalValue = contract.getOutput(0).getValue(); + this.valueToMe = checkNotNull(storedClientChannel.valueToMe); + this.refundTx = checkNotNull(storedClientChannel.refund); + this.refundFees = checkNotNull(storedClientChannel.refundFees); + stateMachine.transition(State.READY); + initWalletListeners(); + } + + public PaymentChannelV2ClientState(Wallet wallet, ECKey myKey, ECKey serverMultisigKey, Coin value, long expiryTimeInSeconds) throws VerificationException { + super(wallet, myKey, serverMultisigKey, value, expiryTimeInSeconds); + checkArgument(value.signum() > 0); + initWalletListeners(); + this.valueToMe = this.totalValue = checkNotNull(value); + this.expiryTime = expiryTimeInSeconds; + stateMachine.transition(State.NEW); + } + + @Override + protected Multimap getStateTransitions() { + Multimap result = MultimapBuilder.enumKeys(State.class).arrayListValues().build(); + result.put(State.UNINITIALISED, State.NEW); + result.put(State.UNINITIALISED, State.READY); + result.put(State.NEW, State.SAVE_STATE_IN_WALLET); + result.put(State.SAVE_STATE_IN_WALLET, State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER); + result.put(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, State.READY); + result.put(State.READY, State.EXPIRED); + result.put(State.READY, State.CLOSED); + return result; + } + + @Override + public int getMajorVersion() { + return 2; + } + + @Override + public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException { + final NetworkParameters params = wallet.getParams(); + Transaction template = new Transaction(params); + // There is also probably a change output, but we don't bother shuffling them as it's obvious from the + // format which one is the change. If we start obfuscating the change output better in future this may + // be worth revisiting. + Script redeemScript = + ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(expiryTime), myKey, serverKey); + TransactionOutput transactionOutput = template.addOutput(totalValue, + ScriptBuilder.createP2SHOutputScript(redeemScript)); + if (transactionOutput.getMinNonDustValue().compareTo(totalValue) > 0) + throw new ValueOutOfRangeException("totalValue too small to use"); + Wallet.SendRequest req = Wallet.SendRequest.forTx(template); + req.coinSelector = AllowUnconfirmedCoinSelector.get(); + editContractSendRequest(req); + req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable. + req.aesKey = userKey; + wallet.completeTx(req); + Coin multisigFee = req.tx.getFee(); + contract = req.tx; + + // Build a refund transaction that protects us in the case of a bad server that's just trying to cause havoc + // by locking up peoples money (perhaps as a precursor to a ransom attempt). We time lock it so the server + // has an assurance that we cannot take back our money by claiming a refund before the channel closes - this + // relies on the fact that since Bitcoin 0.8 time locked transactions are non-final. This will need to change + // in future as it breaks the intended design of timelocking/tx replacement, but for now it simplifies this + // specific protocol somewhat. + refundTx = new Transaction(params); + refundTx.addInput(contract.getOutput(0)).setSequenceNumber(0); // Allow replacement when it's eventually reactivated. + refundTx.setLockTime(expiryTime); + if (totalValue.compareTo(Coin.CENT) < 0) { + // Must pay min fee. + final Coin valueAfterFee = totalValue.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + if (Transaction.MIN_NONDUST_OUTPUT.compareTo(valueAfterFee) > 0) + throw new ValueOutOfRangeException("totalValue too small to use"); + refundTx.addOutput(valueAfterFee, myKey.toAddress(params)); + refundFees = multisigFee.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + } else { + refundTx.addOutput(totalValue, myKey.toAddress(params)); + refundFees = multisigFee; + } + + TransactionSignature refundSignature = + refundTx.calculateSignature(0, myKey.maybeDecrypt(userKey), + getSignedScript(), Transaction.SigHash.ALL, false); + refundTx.getInput(0).setScriptSig(ScriptBuilder.createCLTVPaymentChannelP2SHRefund(refundSignature, redeemScript)); + + refundTx.getConfidence().setSource(TransactionConfidence.Source.SELF); + log.info("initiated channel with contract {}", contract.getHashAsString()); + stateMachine.transition(State.SAVE_STATE_IN_WALLET); + // Client should now call getIncompleteRefundTransaction() and send it to the server. + } + + @Override + protected synchronized Coin getValueToMe() { + return valueToMe; + } + + protected long getExpiryTime() { + return expiryTime; + } + + @Override + public synchronized Transaction getContract() { + checkState(contract != null); + if (stateMachine.getState() == State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER) { + stateMachine.transition(State.READY); + } + return contract; + } + + @Override + protected synchronized Transaction getContractInternal() { + return contract; + } + + protected synchronized Script getContractScript() { + return contract.getOutput(0).getScriptPubKey(); + } + + @Override + protected Script getSignedScript() { + return ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(expiryTime), myKey, serverKey); + } + + @Override + public synchronized Coin getRefundTxFees() { + checkState(getState().compareTo(State.NEW) > 0); + return refundFees; + } + + @VisibleForTesting Transaction getRefundTransaction() { + return refundTx; + } + + @Override + @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, contract.getHash()) == null); + storedChannel = new StoredClientChannel(getMajorVersion(), id, contract, refundTx, myKey, serverKey, valueToMe, refundFees, expiryTime, true); + channels.putChannel(storedChannel); + } + + @Override + public Coin getTotalValue() { + return totalValue; + } +} diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ServerState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ServerState.java new file mode 100644 index 000000000..91fb016ae --- /dev/null +++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ServerState.java @@ -0,0 +1,220 @@ +/* + * 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.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Locale; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * Version 2 of the payment channel state machine - uses CLTV opcode transactions + * instead of multisig transactions. + */ +public class PaymentChannelV2ServerState extends PaymentChannelServerState { + private static final Logger log = LoggerFactory.getLogger(PaymentChannelV1ServerState.class); + + // The total value locked into the CLTV output and the value to us in the last signature the client provided + private Coin feePaidForPayment; + + // The client key for the multi-sig contract + // We currently also use the serverKey for payouts, but this is not required + protected ECKey clientKey; + + PaymentChannelV2ServerState(StoredServerChannel storedServerChannel, Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException { + super(storedServerChannel, wallet, broadcaster); + synchronized (storedServerChannel) { + this.clientKey = storedServerChannel.clientKey; + stateMachine.transition(State.READY); + } + } + + public PaymentChannelV2ServerState(TransactionBroadcaster broadcaster, Wallet wallet, ECKey serverKey, long minExpireTime) { + super(broadcaster, wallet, serverKey, minExpireTime); + stateMachine.transition(State.WAITING_FOR_MULTISIG_CONTRACT); + } + + @Override + public Multimap getStateTransitions() { + Multimap result = MultimapBuilder.enumKeys(State.class).arrayListValues().build(); + result.put(State.UNINITIALISED, State.READY); + result.put(State.UNINITIALISED, State.WAITING_FOR_MULTISIG_CONTRACT); + result.put(State.WAITING_FOR_MULTISIG_CONTRACT, State.WAITING_FOR_MULTISIG_ACCEPTANCE); + result.put(State.WAITING_FOR_MULTISIG_ACCEPTANCE, State.READY); + result.put(State.READY, State.CLOSING); + result.put(State.CLOSING, State.CLOSED); + for (State state : State.values()) { + result.put(state, State.ERROR); + } + return result; + } + + @Override + public int getMajorVersion() { + return 2; + } + + @Override + public TransactionOutput getClientOutput() { + return null; + } + + public void provideClientKey(byte[] clientKey) { + this.clientKey = ECKey.fromPublicOnly(clientKey); + } + + @Override + public synchronized Coin getFeePaid() { + stateMachine.checkState(State.CLOSED, State.CLOSING); + return feePaidForPayment; + } + + @Override + protected Script getSignedScript() { + return createP2SHRedeemScript(); + } + + @Override + protected void verifyContract(final Transaction contract) { + super.verifyContract(contract); + // Check contract matches P2SH hash + byte[] expected = getContractScript().getPubKeyHash(); + byte[] actual = Utils.sha256hash160(createP2SHRedeemScript().getProgram()); + if (!Arrays.equals(actual, expected)) { + throw new VerificationException( + "P2SH hash didn't match required contract - contract should be a CLTV micropayment channel to client and server in that order."); + } + } + + /** + * Creates a P2SH script outputting to the client and server pubkeys + * @return + */ + @Override + protected Script createOutputScript() { + return ScriptBuilder.createP2SHOutputScript(createP2SHRedeemScript()); + } + + private Script createP2SHRedeemScript() { + return ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(getExpiryTime()), clientKey, serverKey); + } + + protected ECKey getClientKey() { + return clientKey; + } + + // Signs the first input of the transaction which must spend the multisig contract. + private void signP2SHInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) { + TransactionSignature signature = tx.calculateSignature(0, serverKey, createP2SHRedeemScript(), hashType, anyoneCanPay); + byte[] mySig = signature.encodeToBitcoin(); + Script scriptSig = ScriptBuilder.createCLTVPaymentChannelP2SHInput(bestValueSignature, mySig, createP2SHRedeemScript()); + tx.getInput(0).setScriptSig(scriptSig); + } + + final SettableFuture closedFuture = SettableFuture.create(); + + @Override + public synchronized ListenableFuture close() throws InsufficientMoneyException { + if (storedServerChannel != null) { + StoredServerChannel temp = storedServerChannel; + storedServerChannel = null; + StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) + wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); + channels.closeChannel(temp); // May call this method again for us (if it wasn't the original caller) + if (getState().compareTo(State.CLOSING) >= 0) + return closedFuture; + } + + if (getState().ordinal() < State.READY.ordinal()) { + log.error("Attempt to settle channel in state " + getState()); + stateMachine.transition(State.CLOSED); + closedFuture.set(null); + return closedFuture; + } + if (getState() != State.READY) { + // TODO: What is this codepath for? + log.warn("Failed attempt to settle a channel in state " + getState()); + return closedFuture; + } + Transaction tx = null; + try { + Wallet.SendRequest req = makeUnsignedChannelContract(bestValueToMe); + tx = req.tx; + // Provide a throwaway signature so that completeTx won't complain out about unsigned inputs it doesn't + // know how to sign. Note that this signature does actually have to be valid, so we can't use a dummy + // signature to save time, because otherwise completeTx will try to re-sign it to make it valid and then + // die. We could probably add features to the SendRequest API to make this a bit more efficient. + signP2SHInput(tx, Transaction.SigHash.NONE, true); + // Let wallet handle adding additional inputs/fee as necessary. + req.shuffleOutputs = false; + req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG; + wallet.completeTx(req); // TODO: Fix things so shuffling is usable. + feePaidForPayment = req.tx.getFee(); + log.info("Calculated fee is {}", feePaidForPayment); + if (feePaidForPayment.compareTo(bestValueToMe) > 0) { + final String msg = String.format(Locale.US, "Had to pay more in fees (%s) than the channel was worth (%s)", + feePaidForPayment, bestValueToMe); + throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg); + } + // Now really sign the multisig input. + signP2SHInput(tx, Transaction.SigHash.ALL, false); + // Some checks that shouldn't be necessary but it can't hurt to check. + tx.verify(); // Sanity check syntax. + for (TransactionInput input : tx.getInputs()) + input.verify(); // Run scripts and ensure it is valid. + } catch (InsufficientMoneyException e) { + throw e; // Don't fall through. + } catch (Exception e) { + log.error("Could not verify self-built tx\nMULTISIG {}\nCLOSE {}", contract, tx != null ? tx : ""); + throw new RuntimeException(e); // Should never happen. + } + stateMachine.transition(State.CLOSING); + log.info("Closing channel, broadcasting tx {}", tx); + // The act of broadcasting the transaction will add it to the wallet. + ListenableFuture future = broadcaster.broadcastTransaction(tx).future(); + Futures.addCallback(future, new FutureCallback() { + @Override public void onSuccess(Transaction transaction) { + log.info("TX {} propagated, channel successfully closed.", transaction.getHash()); + stateMachine.transition(State.CLOSED); + closedFuture.set(transaction); + } + + @Override public void onFailure(Throwable throwable) { + log.error("Failed to settle channel, could not broadcast: {}", throwable); + stateMachine.transition(State.ERROR); + closedFuture.setException(throwable); + } + }); + return closedFuture; + } +} diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/ServerState.java b/core/src/main/java/org/bitcoinj/protocols/channels/ServerState.java index 6868f36b0..37db23c80 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/ServerState.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/ServerState.java @@ -745,11 +745,11 @@ public final class ServerState { com.google.protobuf.ByteString getContractTransaction(); /** - * required 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 An enum of states to use + */ +public class StateMachine> { + private State currentState; + + private final Multimap transitions; + + public StateMachine(State startState, Multimap transitions) { + currentState = checkNotNull(startState); + this.transitions = checkNotNull(transitions); + } + + /** + * Checks that the machine is in the given state. Throws if it isn't. + * @param requiredState + */ + public synchronized void checkState(State requiredState) throws IllegalStateException { + if (requiredState != currentState) { + throw new IllegalStateException(String.format(Locale.US, + "Expected state %s, but in state %s", requiredState, currentState)); + } + } + + /** + * Checks that the machine is in one of the given states. Throws if it isn't. + * @param requiredStates + */ + public synchronized void checkState(State... requiredStates) throws IllegalStateException { + for (State requiredState : requiredStates) { + if (requiredState.equals(currentState)) { + return; + } + } + throw new IllegalStateException(String.format(Locale.US, + "Expected states %s, but in state %s", Lists.newArrayList(requiredStates), currentState)); + } + + /** + * Transitions to a new state, provided that the required transition exists + * @param newState + * @throws IllegalStateException If no state transition exists from oldState to newState + */ + public synchronized void transition(State newState) throws IllegalStateException { + if (transitions.containsEntry(currentState, newState)) { + currentState = newState; + } else { + throw new IllegalStateException(String.format(Locale.US, + "Attempted invalid transition from %s to %s", currentState, newState)); + } + } + + public synchronized State getState() { + return currentState; + } + + @Override + public String toString() { + return new StringBuilder().append('[').append(getState()).append(']').toString(); + } +} diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelClientStates.java b/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelClientStates.java index 152e95d39..48e4282e2 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelClientStates.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelClientStates.java @@ -17,6 +17,8 @@ package org.bitcoinj.protocols.channels; import org.bitcoinj.core.*; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.utils.Threading; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.HashMultimap; @@ -279,14 +281,18 @@ public class StoredPaymentChannelClientStates implements WalletExtension { (!params.hasMaxMoney() || channel.refundFees.compareTo(params.getMaxMoney()) <= 0)); checkNotNull(channel.myKey.getPubKey()); checkState(channel.refund.getConfidence().getSource() == TransactionConfidence.Source.SELF); + checkNotNull(channel.myKey.getPubKey()); final ClientState.StoredClientPaymentChannel.Builder value = ClientState.StoredClientPaymentChannel.newBuilder() + .setMajorVersion(channel.majorVersion) .setId(ByteString.copyFrom(channel.id.getBytes())) .setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize())) + .setRefundFees(channel.refundFees.value) .setRefundTransaction(ByteString.copyFrom(channel.refund.bitcoinSerialize())) .setMyKey(ByteString.copyFrom(new byte[0])) // Not used, but protobuf message requires .setMyPublicKey(ByteString.copyFrom(channel.myKey.getPubKey())) + .setServerKey(ByteString.copyFrom(channel.serverKey.getPubKey())) .setValueToMe(channel.valueToMe.value) - .setRefundFees(channel.refundFees.value); + .setExpiryTime(channel.expiryTime); if (channel.close != null) value.setCloseTransactionHash(ByteString.copyFrom(channel.close.getHash().getBytes())); builder.addChannels(value); @@ -311,12 +317,17 @@ public class StoredPaymentChannelClientStates implements WalletExtension { ECKey myKey = (storedState.getMyKey().isEmpty()) ? containingWallet.findKeyFromPubKey(storedState.getMyPublicKey().toByteArray()) : ECKey.fromPrivate(storedState.getMyKey().toByteArray()); - StoredClientChannel channel = new StoredClientChannel(Sha256Hash.wrap(storedState.getId().toByteArray()), + ECKey serverKey = storedState.hasServerKey() ? ECKey.fromPublicOnly(storedState.getServerKey().toByteArray()) : null; + StoredClientChannel channel = new StoredClientChannel(storedState.getMajorVersion(), + Sha256Hash.wrap(storedState.getId().toByteArray()), params.getDefaultSerializer().makeTransaction(storedState.getContractTransaction().toByteArray()), refundTransaction, myKey, + serverKey, Coin.valueOf(storedState.getValueToMe()), - Coin.valueOf(storedState.getRefundFees()), false); + Coin.valueOf(storedState.getRefundFees()), + storedState.getExpiryTime(), + false); if (storedState.hasCloseTransactionHash()) { Sha256Hash closeTxHash = Sha256Hash.wrap(storedState.getCloseTransactionHash().toByteArray()); channel.close = containingWallet.getTransaction(closeTxHash); @@ -352,29 +363,43 @@ public class StoredPaymentChannelClientStates implements WalletExtension { * when they expire. */ class StoredClientChannel { + int majorVersion; Sha256Hash id; Transaction contract, refund; + // The expiry time of the contract in protocol v2. + long expiryTime; // The transaction that closed the channel (generated by the server) Transaction close; ECKey myKey; + ECKey serverKey; Coin valueToMe, refundFees; // In-memory flag to indicate intent to resume this channel (or that the channel is already in use) boolean active = false; - StoredClientChannel(Sha256Hash id, Transaction contract, Transaction refund, ECKey myKey, Coin valueToMe, - Coin refundFees, boolean active) { + StoredClientChannel(int majorVersion, Sha256Hash id, Transaction contract, Transaction refund, ECKey myKey, ECKey serverKey, Coin valueToMe, + Coin refundFees, long expiryTime, boolean active) { + this.majorVersion = majorVersion; this.id = id; this.contract = contract; this.refund = refund; this.myKey = myKey; + this.serverKey = serverKey; this.valueToMe = valueToMe; this.refundFees = refundFees; + this.expiryTime = expiryTime; this.active = active; } long expiryTimeSeconds() { - return refund.getLockTime() + 60 * 5; + switch (majorVersion) { + case 1: + return refund.getLockTime() + 60 * 5; + case 2: + return expiryTime + 60 * 5; + default: + throw new IllegalStateException("Invalid version"); + } } @Override @@ -382,13 +407,16 @@ class StoredClientChannel { final String newline = String.format(Locale.US, "%n"); final String closeStr = close == null ? "still open" : close.toString().replaceAll(newline, newline + " "); return String.format(Locale.US, "Stored client channel for server ID %s (%s)%n" + + " Version: %d%n" + " Key: %s%n" + + " Server key: %s%n" + " Value left: %s%n" + " Refund fees: %s%n" + + " Expiry : %s%n" + " Contract: %s" + "Refund: %s" + "Close: %s", - id, active ? "active" : "inactive", myKey, valueToMe, refundFees, + id, active ? "active" : "inactive", majorVersion, myKey, serverKey, valueToMe, refundFees, expiryTime, contract.toString().replaceAll(newline, newline + " "), refund.toString().replaceAll(newline, newline + " "), closeStr); diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelServerStates.java b/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelServerStates.java index 257149041..8fd1cdff9 100644 --- a/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelServerStates.java +++ b/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelServerStates.java @@ -91,7 +91,7 @@ public class StoredPaymentChannelServerStates implements WalletExtension { /** *

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 Collection data() { + return Arrays.asList( + PaymentChannelClient.VersionSelector.VERSION_1, + PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1); + } + + @Parameterized.Parameter + public PaymentChannelClient.VersionSelector versionSelector; + + /** + * Returns 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 SettableFuture> serverCloseFuture = SettableFuture.create(); + final SettableFuture> serverCloseFuture = SettableFuture.create(); final SettableFuture channelOpenFuture = SettableFuture.create(); final BlockingQueue q = new LinkedBlockingQueue(); final PaymentChannelServerListener server = new PaymentChannelServerListener(mockBroadcaster, serverWallet, 30, COIN, @@ -162,7 +195,7 @@ public class ChannelConnectionTest extends TestWithWallet { server.bindAndStart(4243); PaymentChannelClientConnection client = new PaymentChannelClientConnection( - new InetSocketAddress("localhost", 4243), 30, wallet, myKey, COIN, "", PaymentChannelClient.DEFAULT_TIME_WINDOW, userKeySetup); + new InetSocketAddress("localhost", 4243), 30, wallet, myKey, COIN, "", PaymentChannelClient.DEFAULT_TIME_WINDOW, userKeySetup, versionSelector); // Wait for the multi-sig tx to be transmitted. broadcastTxPause.release(); @@ -213,6 +246,10 @@ public class ChannelConnectionTest extends TestWithWallet { broadcastTxPause.release(); Transaction settleTx = broadcasts.take(); + assertTrue(serverState.getState() == PaymentChannelServerState.State.CLOSING || + serverState.getState() == PaymentChannelServerState.State.CLOSED); + // Wait for the server thread to catch up with closing + serverState.close().get(); assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState()); if (!serverState.getBestValueToMe().equals(amount) || !serverState.getFeePaid().equals(Coin.ZERO)) fail(); @@ -220,7 +257,7 @@ public class ChannelConnectionTest extends TestWithWallet { // Send the settle TX to the client wallet. sendMoneyToWallet(settleTx, AbstractBlockChain.NewBlockType.BEST_CHAIN); - assertEquals(PaymentChannelClientState.State.CLOSED, client.state().getState()); + assertTrue(client.state().getState() == PaymentChannelClientState.State.CLOSED); server.close(); server.close(); @@ -235,9 +272,13 @@ public class ChannelConnectionTest extends TestWithWallet { @Test public void testServerErrorHandling_badTransaction() throws Exception { + if (!useRefunds()) { + // This test only applies to versions with refunds + return; + } // Gives the server crap and checks proper error responses are sent. ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); PaymentChannelServer server = pair.server; server.connectionOpen(); client.connectionOpen(); @@ -262,7 +303,7 @@ public class ChannelConnectionTest extends TestWithWallet { public void testServerErrorHandling_killSocketOnClose() throws Exception { // Make sure the server closes the socket on CLOSE ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); PaymentChannelServer server = pair.server; server.connectionOpen(); client.connectionOpen(); @@ -280,7 +321,7 @@ public class ChannelConnectionTest extends TestWithWallet { public void testServerErrorHandling_killSocketOnError() throws Exception { // Make sure the server closes the socket on ERROR ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); PaymentChannelServer server = pair.server; server.connectionOpen(); client.connectionOpen(); @@ -304,7 +345,7 @@ public class ChannelConnectionTest extends TestWithWallet { // Open up a normal channel. ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); pair.server.connectionOpen(); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector); PaymentChannelServer server = pair.server; client.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); @@ -312,8 +353,10 @@ public class ChannelConnectionTest extends TestWithWallet { final Protos.TwoWayChannelMessage initiateMsg = pair.serverRecorder.checkNextMsg(MessageType.INITIATE); Coin minPayment = Coin.valueOf(initiateMsg.getInitiate().getMinPayment()); client.receiveMessage(initiateMsg); - server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); - client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); + if (useRefunds()) { + server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); + client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); + } broadcastTxPause.release(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT)); broadcasts.take(); @@ -357,7 +400,7 @@ public class ChannelConnectionTest extends TestWithWallet { // Open up a normal channel. ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); pair.server.connectionOpen(); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector); PaymentChannelServer server = pair.server; client.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); @@ -365,8 +408,10 @@ public class ChannelConnectionTest extends TestWithWallet { final Protos.TwoWayChannelMessage initiateMsg = pair.serverRecorder.checkNextMsg(MessageType.INITIATE); Coin minPayment = Coin.valueOf(initiateMsg.getInitiate().getMinPayment()); client.receiveMessage(initiateMsg); - server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); - client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); + if (useRefunds()) { + server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); + client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); + } broadcastTxPause.release(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT)); broadcasts.take(); @@ -415,7 +460,7 @@ public class ChannelConnectionTest extends TestWithWallet { (StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); - client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder); + client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector); server = pair.server; client.connectionOpen(); server.connectionOpen(); @@ -442,7 +487,7 @@ public class ChannelConnectionTest extends TestWithWallet { // Now open up a new client with the same id and make sure the server disconnects the previous client. pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); - client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder); + client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector); server = pair.server; client.connectionOpen(); server.connectionOpen(); @@ -454,7 +499,7 @@ public class ChannelConnectionTest extends TestWithWallet { } // Make sure the server allows two simultaneous opens. It will close the first and allow resumption of the second. pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); - client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder); + client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector); server = pair.server; client.connectionOpen(); server.connectionOpen(); @@ -484,7 +529,11 @@ public class ChannelConnectionTest extends TestWithWallet { StoredPaymentChannelClientStates newClientStates = new StoredPaymentChannelClientStates(wallet, mockBroadcaster); newClientStates.deserializeWalletExtension(wallet, clientStoredChannels.serializeWalletExtension()); broadcastTxPause.release(); - assertTrue(broadcasts.take().getOutput(0).getScriptPubKey().isSentToMultiSig()); + if (isMultiSigContract()) { + assertTrue(broadcasts.take().getOutput(0).getScriptPubKey().isSentToMultiSig()); + } else { + assertTrue(broadcasts.take().getOutput(0).getScriptPubKey().isPayToScriptHash()); + } broadcastTxPause.release(); assertEquals(TransactionConfidence.Source.SELF, broadcasts.take().getConfidence().getSource()); assertTrue(broadcasts.isEmpty()); @@ -534,7 +583,7 @@ public class ChannelConnectionTest extends TestWithWallet { public void testClientUnknownVersion() throws Exception { // Tests client rejects unknown version ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); client.connectionOpen(); pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION); client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() @@ -554,7 +603,7 @@ public class ChannelConnectionTest extends TestWithWallet { // Tests that clients reject too large time windows ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster, 100); PaymentChannelServer server = pair.server; - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); client.connectionOpen(); server.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); @@ -579,7 +628,7 @@ public class ChannelConnectionTest extends TestWithWallet { public void testValuesAreRespected() throws Exception { ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); PaymentChannelServer server = pair.server; - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); client.connectionOpen(); server.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); @@ -605,7 +654,7 @@ public class ChannelConnectionTest extends TestWithWallet { pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); server = pair.server; final Coin myValue = COIN.multiply(10); - client = new PaymentChannelClient(wallet, myKey, myValue, Sha256Hash.ZERO_HASH, pair.clientRecorder); + client = new PaymentChannelClient(wallet, myKey, myValue, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); client.connectionOpen(); server.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); @@ -616,9 +665,15 @@ public class ChannelConnectionTest extends TestWithWallet { .setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey())) .setMinPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value)) .setType(MessageType.INITIATE).build()); - final Protos.TwoWayChannelMessage provideRefund = pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND); - Transaction refund = new Transaction(params, provideRefund.getProvideRefund().getTx().toByteArray()); - assertEquals(myValue, refund.getOutput(0).getValue()); + if (useRefunds()) { + final Protos.TwoWayChannelMessage provideRefund = pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND); + Transaction refund = new Transaction(params, provideRefund.getProvideRefund().getTx().toByteArray()); + assertEquals(myValue, refund.getOutput(0).getValue()); + } else { + assertEquals(2, client.state().getMajorVersion()); + PaymentChannelV2ClientState state = (PaymentChannelV2ClientState) client.state(); + assertEquals(myValue, state.refundTx.getOutput(0).getValue()); + } } @Test @@ -627,7 +682,7 @@ public class ChannelConnectionTest extends TestWithWallet { emptyWallet.freshReceiveKey(); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); PaymentChannelServer server = pair.server; - PaymentChannelClient client = new PaymentChannelClient(emptyWallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(emptyWallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); client.connectionOpen(); server.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); @@ -649,7 +704,7 @@ public class ChannelConnectionTest extends TestWithWallet { public void testClientRefusesNonCanonicalKey() throws Exception { ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); PaymentChannelServer server = pair.server; - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); client.connectionOpen(); server.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); @@ -667,7 +722,7 @@ public class ChannelConnectionTest extends TestWithWallet { public void testClientResumeNothing() throws Exception { ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); PaymentChannelServer server = pair.server; - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); client.connectionOpen(); server.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); @@ -681,7 +736,7 @@ public class ChannelConnectionTest extends TestWithWallet { @Test public void testClientRandomMessage() throws Exception { ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector); client.connectionOpen(); pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION); @@ -702,14 +757,16 @@ public class ChannelConnectionTest extends TestWithWallet { Sha256Hash someServerId = Sha256Hash.ZERO_HASH; ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); pair.server.connectionOpen(); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector); PaymentChannelServer server = pair.server; client.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE)); - server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); - client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); + if (useRefunds()) { + server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); + client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); + } broadcastTxPause.release(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT)); broadcasts.take(); @@ -733,7 +790,7 @@ public class ChannelConnectionTest extends TestWithWallet { client.connectionClosed(); // Now try opening a new channel with the same server ID and verify the client asks for a new channel. - client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder); + client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector); client.connectionOpen(); Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION); assertFalse(msg.getClientVersion().hasPreviousChannelContractHash()); @@ -748,14 +805,16 @@ public class ChannelConnectionTest extends TestWithWallet { Sha256Hash someServerId = Sha256Hash.ZERO_HASH; ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); pair.server.connectionOpen(); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector); PaymentChannelServer server = pair.server; client.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE)); - server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); - client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); + if (useRefunds()) { + server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); + client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); + } broadcastTxPause.release(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT)); broadcasts.take(); @@ -800,7 +859,7 @@ public class ChannelConnectionTest extends TestWithWallet { Sha256Hash someServerId = Sha256Hash.ZERO_HASH; ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); pair.server.connectionOpen(); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector); PaymentChannelServer server = pair.server; client.connectionOpen(); final Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION); @@ -808,8 +867,10 @@ public class ChannelConnectionTest extends TestWithWallet { server.receiveMessage(msg); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE)); - server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); - client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); + if (useRefunds()) { + server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); + client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); + } broadcastTxPause.release(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT)); broadcasts.take(); @@ -829,7 +890,7 @@ public class ChannelConnectionTest extends TestWithWallet { Sha256Hash someServerId = Sha256Hash.ZERO_HASH; ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); pair.server.connectionOpen(); - PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder); + PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector); PaymentChannelServer server = pair.server; client.connectionOpen(); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java index e30980ebb..dba6a1775 100644 --- a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java +++ b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java @@ -6,8 +6,12 @@ import org.easymock.Capture; import org.easymock.EasyMock; 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 java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage; @@ -17,9 +21,9 @@ import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.replay; import static org.junit.Assert.assertEquals; +@RunWith(Parameterized.class) public class PaymentChannelClientTest { - private static final int CLIENT_MAJOR_VERSION = 1; private Wallet wallet; private ECKey ecKey; private Sha256Hash serverHash; @@ -28,6 +32,22 @@ public class PaymentChannelClientTest { public Capture clientVersionCapture; public int defaultTimeWindow = 86340; + /** + * We use parameterized tests to run the client channel tests with each + * version of the channel. + */ + @Parameterized.Parameters(name = "{index}: PaymentChannelClientTest({0})") + public static Collection data() { + return Arrays.asList( + PaymentChannelClient.VersionSelector.VERSION_1, + PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1, + PaymentChannelClient.VersionSelector.VERSION_2 + ); + } + + @Parameterized.Parameter + public PaymentChannelClient.VersionSelector versionSelector; + @Before public void before() { wallet = createMock(Wallet.class); @@ -40,7 +60,7 @@ public class PaymentChannelClientTest { @Test public void shouldSendClientVersionOnChannelOpen() throws Exception { - PaymentChannelClient dut = new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, connection); + PaymentChannelClient dut = new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, connection, versionSelector); connection.sendToServer(capture(clientVersionCapture)); EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap()); replay(connection, wallet); @@ -52,7 +72,7 @@ public class PaymentChannelClientTest { long timeWindow = 4000; KeyParameter userKey = null; PaymentChannelClient dut = - new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, timeWindow, userKey, connection); + new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, timeWindow, userKey, connection, versionSelector); connection.sendToServer(capture(clientVersionCapture)); EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap()); replay(connection, wallet); @@ -66,7 +86,8 @@ public class PaymentChannelClientTest { assertEquals("Wrong type " + type, CLIENT_VERSION, type); final Protos.ClientVersion clientVersion = response.getClientVersion(); final int major = clientVersion.getMajor(); - assertEquals("Wrong major version " + major, CLIENT_MAJOR_VERSION, major); + final int requestedVersion = versionSelector.getRequestedMajorVersion(); + assertEquals("Wrong major version " + major, requestedVersion, major); final long actualTimeWindow = clientVersion.getTimeWindowSecs(); assertEquals("Wrong timeWindow " + actualTimeWindow, expectedTimeWindow, actualTimeWindow ); } diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelServerTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelServerTest.java index 7e0b9c6cc..c94f74112 100644 --- a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelServerTest.java +++ b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelServerTest.java @@ -8,6 +8,11 @@ import org.bitcoin.paymentchannel.Protos; import org.easymock.Capture; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; import static junit.framework.TestCase.assertTrue; import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage; @@ -15,10 +20,8 @@ import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType import static org.easymock.EasyMock.*; import static org.junit.Assert.assertEquals; +@RunWith(Parameterized.class) public class PaymentChannelServerTest { - - private static final int CLIENT_MAJOR_VERSION = 1; - private static final long SERVER_MAJOR_VERSION = 1; public Wallet wallet; public PaymentChannelServer.ServerConnection connection; public PaymentChannelServer dut; @@ -35,6 +38,17 @@ public class PaymentChannelServerTest { Utils.setMockClock(); } + /** + * We use parameterized tests to run the client channel tests with each + * version of the channel. + */ + @Parameterized.Parameters(name = "{index}: PaymentChannelServerTest(version {0})") + public static Collection data() { + return Arrays.asList(1, 2); + } + + @Parameterized.Parameter + public int protocolVersion; @Test public void shouldAcceptDefaultTimeWindow() { @@ -123,7 +137,7 @@ public class PaymentChannelServerTest { final MessageType type = response.getType(); assertEquals("Wrong type " + type, MessageType.SERVER_VERSION, type); final long major = response.getServerVersion().getMajor(); - assertEquals("Wrong major version", SERVER_MAJOR_VERSION, major); + assertEquals("Wrong major version", protocolVersion, major); } private void assertExpireTime(long expectedExpire, Capture initiateCapture) { @@ -136,12 +150,12 @@ public class PaymentChannelServerTest { } private TwoWayChannelMessage createClientVersionMessage() { - final Protos.ClientVersion.Builder clientVersion = Protos.ClientVersion.newBuilder().setMajor(CLIENT_MAJOR_VERSION); + final Protos.ClientVersion.Builder clientVersion = Protos.ClientVersion.newBuilder().setMajor(protocolVersion); return TwoWayChannelMessage.newBuilder().setType(MessageType.CLIENT_VERSION).setClientVersion(clientVersion).build(); } private TwoWayChannelMessage createClientVersionMessage(long timeWindow) { - final Protos.ClientVersion.Builder clientVersion = Protos.ClientVersion.newBuilder().setMajor(CLIENT_MAJOR_VERSION); + final Protos.ClientVersion.Builder clientVersion = Protos.ClientVersion.newBuilder().setMajor(protocolVersion); if (timeWindow > 0) clientVersion.setTimeWindowSecs(timeWindow); return TwoWayChannelMessage.newBuilder().setType(MessageType.CLIENT_VERSION).setClientVersion(clientVersion).build(); } diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java index 687467c50..b53cb021e 100644 --- a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java +++ b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java @@ -26,9 +26,12 @@ import com.google.common.util.concurrent.SettableFuture; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import java.math.BigInteger; import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; @@ -39,6 +42,7 @@ import static org.bitcoinj.testing.FakeTxBuilder.createFakeTx; import static org.bitcoinj.testing.FakeTxBuilder.makeSolvedTestBlock; import static org.junit.Assert.*; +@RunWith(Parameterized.class) public class PaymentChannelStateTest extends TestWithWallet { private ECKey serverKey; private Coin halfCoin; @@ -48,6 +52,27 @@ public class PaymentChannelStateTest extends TestWithWallet { private TransactionBroadcaster mockBroadcaster; private BlockingQueue broadcasts; + /** + * We use parameterized tests to run the channel connection tests with each + * version of the channel. + */ + @Parameterized.Parameters(name = "{index}: PaymentChannelStateTest({0})") + public static Collection data() { + return Arrays.asList( + PaymentChannelClient.VersionSelector.VERSION_1, + PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1); + } + + @Parameterized.Parameter + public PaymentChannelClient.VersionSelector versionSelector; + + /** + * Returns true 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 future; @@ -61,6 +86,7 @@ public class PaymentChannelStateTest extends TestWithWallet { @Override @Before public void setUp() throws Exception { + Utils.setMockClock(); // Use mock clock super.setUp(); wallet.addExtension(new StoredPaymentChannelClientStates(wallet, new TransactionBroadcaster() { @Override @@ -93,13 +119,93 @@ public class PaymentChannelStateTest extends TestWithWallet { super.tearDown(); } + private PaymentChannelClientState makeClientState(Wallet wallet, ECKey myKey, ECKey serverKey, Coin value, long time) { + switch (versionSelector) { + case VERSION_1: + return new PaymentChannelV1ClientState(wallet, myKey, serverKey, value, time); + case VERSION_2_ALLOW_1: + case VERSION_2: + return new PaymentChannelV2ClientState(wallet, myKey, serverKey, value, time); + default: + return null; + } + } + + private PaymentChannelServerState makeServerState(TransactionBroadcaster broadcaster, Wallet wallet, ECKey serverKey, long time) { + switch (versionSelector) { + case VERSION_1: + return new PaymentChannelV1ServerState(broadcaster, wallet, serverKey, time); + case VERSION_2_ALLOW_1: + case VERSION_2: + return new PaymentChannelV2ServerState(broadcaster, wallet, serverKey, time); + default: + return null; + } + } + + private PaymentChannelV1ClientState clientV1State() { + if (clientState instanceof PaymentChannelV1ClientState) { + return (PaymentChannelV1ClientState) clientState; + } else { + return null; + } + } + + private PaymentChannelV1ServerState serverV1State() { + if (serverState instanceof PaymentChannelV1ServerState) { + return (PaymentChannelV1ServerState) serverState; + } else { + return null; + } + } + + private PaymentChannelV2ClientState clientV2State() { + if (clientState instanceof PaymentChannelV2ClientState) { + return (PaymentChannelV2ClientState) clientState; + } else { + return null; + } + } + + private PaymentChannelV2ServerState serverV2State() { + if (serverState instanceof PaymentChannelV2ServerState) { + return (PaymentChannelV2ServerState) serverState; + } else { + return null; + } + } + + private PaymentChannelServerState.State getInitialServerState() { + switch (versionSelector) { + case VERSION_1: + return PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION; + case VERSION_2_ALLOW_1: + case VERSION_2: + return PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT; + default: + return null; + } + } + + private PaymentChannelClientState.State getInitialClientState() { + switch (versionSelector) { + case VERSION_1: + return PaymentChannelClientState.State.INITIATED; + case VERSION_2_ALLOW_1: + case VERSION_2: + return PaymentChannelClientState.State.SAVE_STATE_IN_WALLET; + default: + return null; + } + } + @Test public void stateErrors() throws Exception { - PaymentChannelClientState channelState = new PaymentChannelClientState(wallet, myKey, serverKey, + PaymentChannelClientState channelState = makeClientState(wallet, myKey, serverKey, COIN.multiply(10), 20); assertEquals(PaymentChannelClientState.State.NEW, channelState.getState()); try { - channelState.getMultisigContract(); + channelState.getContract(); fail(); } catch (IllegalStateException e) { // Expected. @@ -117,36 +223,48 @@ public class PaymentChannelStateTest extends TestWithWallet { Utils.setMockClock(); // Use mock clock final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24; - serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(getInitialServerState(), serverState.getState()); - clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME); + clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); clientState.initiate(); - assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + assertEquals(getInitialClientState(), clientState.getState()); // Send the refund tx from client to server and get back the signature. - Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); - byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); - // This verifies that the refund can spend the multi-sig output when run. - clientState.provideRefundSignature(refundSig, null); + Transaction refund; + if (useRefunds()) { + refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize()); + byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + // This verifies that the refund can spend the multi-sig output when run. + clientV1State().provideRefundSignature(refundSig, null); + } else { + refund = clientV2State().getRefundTransaction(); + } assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState()); clientState.fakeSave(); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); // Validate the multisig contract looks right. - Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize()); + Transaction multisigContract = new Transaction(params, clientState.getContract().bitcoinSerialize()); assertEquals(PaymentChannelClientState.State.READY, clientState.getState()); assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. Script script = multisigContract.getOutput(0).getScriptPubKey(); - assertTrue(script.isSentToMultiSig()); + if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) { + assertTrue(script.isSentToMultiSig()); + } else { + assertTrue(script.isPayToScriptHash()); + } script = multisigContract.getOutput(1).getScriptPubKey(); assertTrue(script.isSentToAddress()); assertTrue(wallet.getPendingTransactions().contains(multisigContract)); // Provide the server with the multisig contract and simulate successful propagation/acceptance. - serverState.provideMultiSigContract(multisigContract); + if (!useRefunds()) { + serverV2State().provideClientKey(clientState.myKey.getPubKey()); + } + serverState.provideContract(multisigContract); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); final TxFuturePair pair = broadcasts.take(); pair.future.set(pair.tx); @@ -156,12 +274,12 @@ public class PaymentChannelStateTest extends TestWithWallet { assertEquals(2, wallet.getTransactions(false).size()); Iterator walletTransactionIterator = wallet.getTransactions(false).iterator(); Transaction clientWalletMultisigContract = walletTransactionIterator.next(); - assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); + assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash())); if (!clientWalletMultisigContract.getHash().equals(multisigContract.getHash())) { clientWalletMultisigContract = walletTransactionIterator.next(); - assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); + assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash())); } else - assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getCompletedRefundTransaction().getHash())); + assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getRefundTransaction().getHash())); assertEquals(multisigContract.getHash(), clientWalletMultisigContract.getHash()); assertFalse(clientWalletMultisigContract.getInput(0).getConnectedOutput().getSpentBy().getParentTransaction().getHash().equals(refund.getHash())); @@ -231,40 +349,49 @@ public class PaymentChannelStateTest extends TestWithWallet { Utils.setMockClock(); // Use mock clock final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24; - serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(getInitialServerState(), serverState.getState()); - clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), + clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT.divide(2), EXPIRE_TIME); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(CENT.divide(2), clientState.getTotalValue()); clientState.initiate(); // We will have to pay min_tx_fee twice - both the multisig contract and the refund tx assertEquals(clientState.getRefundTxFees(), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(2)); - assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + assertEquals(getInitialClientState(), clientState.getState()); - // Send the refund tx from client to server and get back the signature. - Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); - byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); - // This verifies that the refund can spend the multi-sig output when run. - clientState.provideRefundSignature(refundSig, null); + if (useRefunds()) { + // Send the refund tx from client to server and get back the signature. + Transaction refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize()); + byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + // This verifies that the refund can spend the multi-sig output when run. + clientV1State().provideRefundSignature(refundSig, null); + } assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState()); clientState.fakeSave(); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); // Validate the multisig contract looks right. - Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize()); + Transaction multisigContract = new Transaction(params, clientState.getContract().bitcoinSerialize()); assertEquals(PaymentChannelClientState.State.READY, clientState.getState()); assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. Script script = multisigContract.getOutput(0).getScriptPubKey(); - assertTrue(script.isSentToMultiSig()); + if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) { + assertTrue(script.isSentToMultiSig()); + } else { + assertTrue(script.isPayToScriptHash()); + } script = multisigContract.getOutput(1).getScriptPubKey(); assertTrue(script.isSentToAddress()); assertTrue(wallet.getPendingTransactions().contains(multisigContract)); // Provide the server with the multisig contract and simulate successful propagation/acceptance. - serverState.provideMultiSigContract(multisigContract); + if (!useRefunds()) { + serverV2State().provideClientKey(clientState.myKey.getPubKey()); + } + serverState.provideContract(multisigContract); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); final TxFuturePair pop = broadcasts.take(); pop.future.set(pop.tx); @@ -301,7 +428,7 @@ public class PaymentChannelStateTest extends TestWithWallet { clientBroadcastedMultiSig.future.set(clientBroadcastedMultiSig.tx); Transaction clientBroadcastedRefund = broadcastRefund.tx; - assertEquals(clientBroadcastedRefund.getHash(), clientState.getCompletedRefundTransaction().getHash()); + assertEquals(clientBroadcastedRefund.getHash(), clientState.getRefundTransaction().getHash()); for (TransactionInput input : clientBroadcastedRefund.getInputs()) { // If the multisig output is connected, the wallet will fail to deserialize if (input.getOutpoint().getHash().equals(clientBroadcastedMultiSig.tx.getHash())) @@ -332,93 +459,122 @@ public class PaymentChannelStateTest extends TestWithWallet { Utils.setMockClock(); // Use mock clock final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24; - serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(getInitialServerState(), serverState.getState()); - clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME); + clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); clientState.initiate(); - assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + assertEquals(getInitialClientState(), clientState.getState()); - // Test refund transaction with any number of issues - byte[] refundTxBytes = clientState.getIncompleteRefundTransaction().bitcoinSerialize(); - Transaction refund = new Transaction(params, refundTxBytes); - refund.addOutput(Coin.ZERO, new ECKey().toAddress(params)); - try { - serverState.provideRefundTransaction(refund, myKey.getPubKey()); - fail(); - } catch (VerificationException e) {} + if (useRefunds()) { + // Test refund transaction with any number of issues + byte[] refundTxBytes = clientV1State().getIncompleteRefundTransaction().bitcoinSerialize(); + Transaction refund = new Transaction(params, refundTxBytes); + refund.addOutput(Coin.ZERO, new ECKey().toAddress(params)); + try { + serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + fail(); + } catch (VerificationException e) { + } - refund = new Transaction(params, refundTxBytes); - refund.addInput(new TransactionInput(params, refund, new byte[] {}, new TransactionOutPoint(params, 42, refund.getHash()))); - try { - serverState.provideRefundTransaction(refund, myKey.getPubKey()); - fail(); - } catch (VerificationException e) {} + refund = new Transaction(params, refundTxBytes); + refund.addInput(new TransactionInput(params, refund, new byte[]{}, new TransactionOutPoint(params, 42, refund.getHash()))); + try { + serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + fail(); + } catch (VerificationException e) { + } - refund = new Transaction(params, refundTxBytes); - refund.setLockTime(0); - try { - serverState.provideRefundTransaction(refund, myKey.getPubKey()); - fail(); - } catch (VerificationException e) {} + refund = new Transaction(params, refundTxBytes); + refund.setLockTime(0); + try { + serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + fail(); + } catch (VerificationException e) { + } - refund = new Transaction(params, refundTxBytes); - refund.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE); - try { - serverState.provideRefundTransaction(refund, myKey.getPubKey()); - fail(); - } catch (VerificationException e) {} + refund = new Transaction(params, refundTxBytes); + refund.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE); + try { + serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + fail(); + } catch (VerificationException e) { + } - refund = new Transaction(params, refundTxBytes); - byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); - try { serverState.provideRefundTransaction(refund, myKey.getPubKey()); fail(); } catch (IllegalStateException e) {} - assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + refund = new Transaction(params, refundTxBytes); + byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + try { + serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + fail(); + } catch (IllegalStateException e) { + } + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); - byte[] refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); - refundSigCopy[refundSigCopy.length-1] = (byte) (Transaction.SigHash.NONE.ordinal() + 1); - try { - clientState.provideRefundSignature(refundSigCopy, null); - fail(); - } catch (VerificationException e) { - assertTrue(e.getMessage().contains("SIGHASH_NONE")); + byte[] refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); + refundSigCopy[refundSigCopy.length - 1] = (byte) (Transaction.SigHash.NONE.ordinal() + 1); + try { + clientV1State().provideRefundSignature(refundSigCopy, null); + fail(); + } catch (VerificationException e) { + assertTrue(e.getMessage().contains("SIGHASH_NONE")); + } + + refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); + refundSigCopy[3] ^= 0x42; // Make the signature fail standard checks + try { + clientV1State().provideRefundSignature(refundSigCopy, null); + fail(); + } catch (VerificationException e) { + assertTrue(e.getMessage().contains("not canonical")); + } + + refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); + refundSigCopy[10] ^= 0x42; // Flip some random bits in the signature (to make it invalid, not just nonstandard) + try { + clientV1State().provideRefundSignature(refundSigCopy, null); + fail(); + } catch (VerificationException e) { + assertFalse(e.getMessage().contains("not canonical")); + } + + refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); + try { + clientV1State().getCompletedRefundTransaction(); + fail(); + } catch (IllegalStateException e) { + } + clientV1State().provideRefundSignature(refundSigCopy, null); + try { + clientV1State().provideRefundSignature(refundSigCopy, null); + fail(); + } catch (IllegalStateException e) { + } } - - refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); - refundSigCopy[3] ^= 0x42; // Make the signature fail standard checks - try { - clientState.provideRefundSignature(refundSigCopy, null); - fail(); - } catch (VerificationException e) { - assertTrue(e.getMessage().contains("not canonical")); - } - - refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); - refundSigCopy[10] ^= 0x42; // Flip some random bits in the signature (to make it invalid, not just nonstandard) - try { - clientState.provideRefundSignature(refundSigCopy, null); - fail(); - } catch (VerificationException e) { - assertFalse(e.getMessage().contains("not canonical")); - } - - refundSigCopy = Arrays.copyOf(refundSig, refundSig.length); - try { clientState.getCompletedRefundTransaction(); fail(); } catch (IllegalStateException e) {} - clientState.provideRefundSignature(refundSigCopy, null); - try { clientState.provideRefundSignature(refundSigCopy, null); fail(); } catch (IllegalStateException e) {} assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState()); clientState.fakeSave(); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); + if (!useRefunds()) { + serverV2State().provideClientKey(myKey.getPubKey()); + } + try { clientState.incrementPaymentBy(Coin.SATOSHI, null); fail(); } catch (IllegalStateException e) {} - byte[] multisigContractSerialized = clientState.getMultisigContract().bitcoinSerialize(); + byte[] multisigContractSerialized = clientState.getContract().bitcoinSerialize(); Transaction multisigContract = new Transaction(params, multisigContractSerialized); multisigContract.clearOutputs(); - multisigContract.addOutput(halfCoin, ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(serverKey, myKey))); + // Swap order of client and server keys to check correct failure + if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) { + multisigContract.addOutput(halfCoin, ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(serverKey, myKey))); + } else { + multisigContract.addOutput(halfCoin, + ScriptBuilder.createP2SHOutputScript( + ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(serverState.getExpiryTime()), serverKey, myKey))); + } try { - serverState.provideMultiSigContract(multisigContract); + serverState.provideContract(multisigContract); fail(); } catch (VerificationException e) { assertTrue(e.getMessage().contains("client and server in that order")); @@ -426,9 +582,15 @@ public class PaymentChannelStateTest extends TestWithWallet { multisigContract = new Transaction(params, multisigContractSerialized); multisigContract.clearOutputs(); - multisigContract.addOutput(Coin.ZERO, ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(myKey, serverKey))); + if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) { + multisigContract.addOutput(Coin.ZERO, ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(myKey, serverKey))); + } else { + multisigContract.addOutput(Coin.ZERO, + ScriptBuilder.createP2SHOutputScript( + ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(serverState.getExpiryTime()), myKey, serverKey))); + } try { - serverState.provideMultiSigContract(multisigContract); + serverState.provideContract(multisigContract); fail(); } catch (VerificationException e) { assertTrue(e.getMessage().contains("zero value")); @@ -438,13 +600,13 @@ public class PaymentChannelStateTest extends TestWithWallet { multisigContract.clearOutputs(); multisigContract.addOutput(new TransactionOutput(params, multisigContract, halfCoin, new byte[] {0x01})); try { - serverState.provideMultiSigContract(multisigContract); + serverState.provideContract(multisigContract); fail(); } catch (VerificationException e) {} multisigContract = new Transaction(params, multisigContractSerialized); - ListenableFuture multisigStateFuture = serverState.provideMultiSigContract(multisigContract); - try { serverState.provideMultiSigContract(multisigContract); fail(); } catch (IllegalStateException e) {} + ListenableFuture multisigStateFuture = serverState.provideContract(multisigContract); + try { serverState.provideContract(multisigContract); fail(); } catch (IllegalStateException e) {} assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); assertFalse(multisigStateFuture.isDone()); final TxFuturePair pair = broadcasts.take(); @@ -536,18 +698,18 @@ public class PaymentChannelStateTest extends TestWithWallet { Utils.setMockClock(); // Use mock clock final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24; - serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(getInitialServerState(), serverState.getState()); // Clearly SATOSHI is far too small to be useful - clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), Coin.SATOSHI, EXPIRE_TIME); + clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), Coin.SATOSHI, EXPIRE_TIME); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); try { clientState.initiate(); fail(); } catch (ValueOutOfRangeException e) {} - clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), + clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), Transaction.MIN_NONDUST_OUTPUT.subtract(Coin.SATOSHI).add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), EXPIRE_TIME); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); @@ -557,37 +719,42 @@ public class PaymentChannelStateTest extends TestWithWallet { } catch (ValueOutOfRangeException e) {} // Verify that MIN_NONDUST_OUTPUT + MIN_TX_FEE is accepted - clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), + clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), Transaction.MIN_NONDUST_OUTPUT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), EXPIRE_TIME); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); // We'll have to pay REFERENCE_DEFAULT_MIN_TX_FEE twice (multisig+refund), and we'll end up getting back nearly nothing... clientState.initiate(); assertEquals(clientState.getRefundTxFees(), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(2)); - assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + assertEquals(getInitialClientState(), clientState.getState()); // Now actually use a more useful CENT - clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME); + clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); clientState.initiate(); assertEquals(clientState.getRefundTxFees(), Coin.ZERO); - assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + assertEquals(getInitialClientState(), clientState.getState()); - // Send the refund tx from client to server and get back the signature. - Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); - byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); - // This verifies that the refund can spend the multi-sig output when run. - clientState.provideRefundSignature(refundSig, null); + if (useRefunds()) { + // Send the refund tx from client to server and get back the signature. + Transaction refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize()); + byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + // This verifies that the refund can spend the multi-sig output when run. + clientV1State().provideRefundSignature(refundSig, null); + } assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState()); clientState.fakeSave(); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); // Get the multisig contract - Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize()); + Transaction multisigContract = new Transaction(params, clientState.getContract().bitcoinSerialize()); assertEquals(PaymentChannelClientState.State.READY, clientState.getState()); // Provide the server with the multisig contract and simulate successful propagation/acceptance. - serverState.provideMultiSigContract(multisigContract); + if (!useRefunds()) { + serverV2State().provideClientKey(clientState.myKey.getPubKey()); + } + serverState.provideContract(multisigContract); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); TxFuturePair pair = broadcasts.take(); pair.future.set(pair.tx); @@ -639,41 +806,63 @@ public class PaymentChannelStateTest extends TestWithWallet { Utils.setMockClock(); // Use mock clock final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24; - serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(getInitialServerState(), serverState.getState()); - clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) { - @Override - protected void editContractSendRequest(Wallet.SendRequest req) { - req.coinSelector = wallet.getCoinSelector(); - } - }; + switch (versionSelector) { + case VERSION_1: + clientState = new PaymentChannelV1ClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) { + @Override + protected void editContractSendRequest(Wallet.SendRequest req) { + req.coinSelector = wallet.getCoinSelector(); + } + }; + break; + case VERSION_2_ALLOW_1: + case VERSION_2: + clientState = new PaymentChannelV2ClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) { + @Override + protected void editContractSendRequest(Wallet.SendRequest req) { + req.coinSelector = wallet.getCoinSelector(); + } + }; + break; + } assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); clientState.initiate(); - assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + assertEquals(getInitialClientState(), clientState.getState()); - // Send the refund tx from client to server and get back the signature. - Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); - byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); - // This verifies that the refund can spend the multi-sig output when run. - clientState.provideRefundSignature(refundSig, null); + if (useRefunds()) { + // Send the refund tx from client to server and get back the signature. + Transaction refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize()); + byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + // This verifies that the refund can spend the multi-sig output when run. + clientV1State().provideRefundSignature(refundSig, null); + } assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState()); clientState.fakeSave(); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); // Validate the multisig contract looks right. - Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize()); - assertEquals(PaymentChannelClientState.State.READY, clientState.getState()); + Transaction multisigContract = new Transaction(params, clientState.getContract().bitcoinSerialize()); + assertEquals(PaymentChannelV1ClientState.State.READY, clientState.getState()); assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. Script script = multisigContract.getOutput(0).getScriptPubKey(); - assertTrue(script.isSentToMultiSig()); + if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) { + assertTrue(script.isSentToMultiSig()); + } else { + assertTrue(script.isPayToScriptHash()); + } script = multisigContract.getOutput(1).getScriptPubKey(); assertTrue(script.isSentToAddress()); assertTrue(wallet.getPendingTransactions().contains(multisigContract)); // Provide the server with the multisig contract and simulate successful propagation/acceptance. - serverState.provideMultiSigContract(multisigContract); + if (!useRefunds()) { + serverV2State().provideClientKey(clientState.myKey.getPubKey()); + } + serverState.provideContract(multisigContract); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); TxFuturePair pair = broadcasts.take(); pair.future.set(pair.tx); @@ -725,36 +914,48 @@ public class PaymentChannelStateTest extends TestWithWallet { Utils.setMockClock(); // Use mock clock final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24; - serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); + serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); + assertEquals(getInitialServerState(), serverState.getState()); - clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME); + clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); clientState.initiate(); - assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); + assertEquals(getInitialClientState(), clientState.getState()); - // Send the refund tx from client to server and get back the signature. - Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); - byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); - assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); - // This verifies that the refund can spend the multi-sig output when run. - clientState.provideRefundSignature(refundSig, null); + Transaction refund; + if (useRefunds()) { + refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize()); + // Send the refund tx from client to server and get back the signature. + byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey()); + assertEquals(PaymentChannelV1ServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); + // This verifies that the refund can spend the multi-sig output when run. + clientV1State().provideRefundSignature(refundSig, null); + } else { + refund = clientV2State().getRefundTransaction(); + } assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState()); clientState.fakeSave(); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); // Validate the multisig contract looks right. - Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize()); + Transaction multisigContract = new Transaction(params, clientState.getContract().bitcoinSerialize()); assertEquals(PaymentChannelClientState.State.READY, clientState.getState()); assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. Script script = multisigContract.getOutput(0).getScriptPubKey(); - assertTrue(script.isSentToMultiSig()); + if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) { + assertTrue(script.isSentToMultiSig()); + } else { + assertTrue(script.isPayToScriptHash()); + } script = multisigContract.getOutput(1).getScriptPubKey(); assertTrue(script.isSentToAddress()); assertTrue(wallet.getPendingTransactions().contains(multisigContract)); // Provide the server with the multisig contract and simulate successful propagation/acceptance. - serverState.provideMultiSigContract(multisigContract); + if (!useRefunds()) { + serverV2State().provideClientKey(clientState.myKey.getPubKey()); + } + serverState.provideContract(multisigContract); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); final TxFuturePair pair = broadcasts.take(); pair.future.set(pair.tx); @@ -764,12 +965,12 @@ public class PaymentChannelStateTest extends TestWithWallet { assertEquals(2, wallet.getTransactions(false).size()); Iterator walletTransactionIterator = wallet.getTransactions(false).iterator(); Transaction clientWalletMultisigContract = walletTransactionIterator.next(); - assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); + assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash())); if (!clientWalletMultisigContract.getHash().equals(multisigContract.getHash())) { clientWalletMultisigContract = walletTransactionIterator.next(); - assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); + assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash())); } else - assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getCompletedRefundTransaction().getHash())); + assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getRefundTransaction().getHash())); assertEquals(multisigContract.getHash(), clientWalletMultisigContract.getHash()); assertFalse(clientWalletMultisigContract.getInput(0).getConnectedOutput().getSpentBy().getParentTransaction().getHash().equals(refund.getHash())); diff --git a/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelClient.java b/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelClient.java index 01b6a336e..a85f78a77 100644 --- a/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelClient.java +++ b/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelClient.java @@ -20,6 +20,7 @@ package org.bitcoinj.examples; import org.bitcoinj.core.*; import org.bitcoinj.kits.WalletAppKit; import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.protocols.channels.PaymentChannelClient; import org.bitcoinj.protocols.channels.PaymentChannelClientConnection; import org.bitcoinj.protocols.channels.StoredPaymentChannelClientStates; import org.bitcoinj.protocols.channels.ValueOutOfRangeException; @@ -113,8 +114,9 @@ public class ExamplePaymentChannelClient { } private void openAndSend(int timeoutSecs, InetSocketAddress server, String channelID, final int times) throws IOException, ValueOutOfRangeException, InterruptedException { + // Use protocol version 1 for simplicity PaymentChannelClientConnection client = new PaymentChannelClientConnection( - server, timeoutSecs, appKit.wallet(), myKey, channelSize, channelID); + server, timeoutSecs, appKit.wallet(), myKey, channelSize, channelID, PaymentChannelClient.VersionSelector.VERSION_1); // Opening the channel requires talking to the server, so it's asynchronous. final CountDownLatch latch = new CountDownLatch(1); Futures.addCallback(client.getChannelOpenFuture(), new FutureCallback() { diff --git a/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelServer.java b/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelServer.java index 37d035e22..635a46623 100644 --- a/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelServer.java +++ b/examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelServer.java @@ -97,10 +97,10 @@ public class ExamplePaymentChannelServer implements PaymentChannelServerListener log.info(" with a maximum value of {}, expiring at UNIX timestamp {}.", // The channel's maximum value is the value of the multisig contract which locks in some // amount of money to the channel - state.getMultisigContract().getOutput(0).getValue(), + state.getContract().getOutput(0).getValue(), // The channel expires at some offset from when the client's refund transaction becomes // spendable. - state.getRefundTransactionUnlockTime() + StoredPaymentChannelServerStates.CHANNEL_EXPIRE_OFFSET); + state.getExpiryTime() + StoredPaymentChannelServerStates.CHANNEL_EXPIRE_OFFSET); } @Override diff --git a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java index f5edc90c6..4a13947bc 100644 --- a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java +++ b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java @@ -724,7 +724,7 @@ public class WalletTool { throw new RuntimeException(e); } - Wallet.SendRequest req = Wallet.SendRequest.toCLTVPaymentChannel(params, lockTime, refundKey, outputKey, value); + Wallet.SendRequest req = Wallet.SendRequest.toCLTVPaymentChannel(params, BigInteger.valueOf(lockTime), refundKey, outputKey, value); if (req.tx.getOutputs().size() == 1 && req.tx.getOutput(0).getValue().equals(wallet.getBalance())) { log.info("Emptying out wallet, recipient may get less than what you expect"); req.emptyWallet = true;