3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-15 03:35:52 +00:00

Implemented version 2 of payment channels API

I implemented version 2 of the payment channels API using
OP_CHECKLOCKTIMEVERIFY-style payment channels.
This commit is contained in:
Will Shackleton 2015-11-22 15:53:23 +00:00 committed by Andreas Schildbach
parent 4b2afc9667
commit 8af0fa9884
28 changed files with 2993 additions and 781 deletions

View File

@ -6006,11 +6006,17 @@ public final class Protos {
* *
* <pre> * <pre>
* The serialized bytes of the transaction in Satoshi format. * 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 * * 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 * 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 * * 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 * 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") * 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.
* </pre> * </pre>
*/ */
boolean hasTx(); boolean hasTx();
@ -6019,11 +6025,17 @@ public final class Protos {
* *
* <pre> * <pre>
* The serialized bytes of the transaction in Satoshi format. * 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 * * 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 * 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 * * 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 * 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") * 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.
* </pre> * </pre>
*/ */
com.google.protobuf.ByteString getTx(); com.google.protobuf.ByteString getTx();
@ -6061,6 +6073,29 @@ public final class Protos {
* </pre> * </pre>
*/ */
org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder getInitialPaymentOrBuilder(); org.bitcoin.paymentchannel.Protos.UpdatePaymentOrBuilder getInitialPaymentOrBuilder();
/**
* <code>optional bytes client_key = 3;</code>
*
* <pre>
* 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.
* </pre>
*/
boolean hasClientKey();
/**
* <code>optional bytes client_key = 3;</code>
*
* <pre>
* 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.
* </pre>
*/
com.google.protobuf.ByteString getClientKey();
} }
/** /**
* Protobuf type {@code paymentchannels.ProvideContract} * Protobuf type {@code paymentchannels.ProvideContract}
@ -6136,6 +6171,11 @@ public final class Protos {
bitField0_ |= 0x00000002; bitField0_ |= 0x00000002;
break; break;
} }
case 26: {
bitField0_ |= 0x00000004;
clientKey_ = input.readBytes();
break;
}
} }
} }
} catch (com.google.protobuf.InvalidProtocolBufferException e) { } catch (com.google.protobuf.InvalidProtocolBufferException e) {
@ -6183,11 +6223,17 @@ public final class Protos {
* *
* <pre> * <pre>
* The serialized bytes of the transaction in Satoshi format. * 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 * * 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 * 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 * * 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 * 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") * 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.
* </pre> * </pre>
*/ */
public boolean hasTx() { public boolean hasTx() {
@ -6198,11 +6244,17 @@ public final class Protos {
* *
* <pre> * <pre>
* The serialized bytes of the transaction in Satoshi format. * 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 * * 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 * 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 * * 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 * 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") * 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.
* </pre> * </pre>
*/ */
public com.google.protobuf.ByteString getTx() { public com.google.protobuf.ByteString getTx() {
@ -6251,9 +6303,39 @@ public final class Protos {
return initialPayment_; return initialPayment_;
} }
public static final int CLIENT_KEY_FIELD_NUMBER = 3;
private com.google.protobuf.ByteString clientKey_;
/**
* <code>optional bytes client_key = 3;</code>
*
* <pre>
* 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.
* </pre>
*/
public boolean hasClientKey() {
return ((bitField0_ & 0x00000004) == 0x00000004);
}
/**
* <code>optional bytes client_key = 3;</code>
*
* <pre>
* 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.
* </pre>
*/
public com.google.protobuf.ByteString getClientKey() {
return clientKey_;
}
private void initFields() { private void initFields() {
tx_ = com.google.protobuf.ByteString.EMPTY; tx_ = com.google.protobuf.ByteString.EMPTY;
initialPayment_ = org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance(); initialPayment_ = org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance();
clientKey_ = com.google.protobuf.ByteString.EMPTY;
} }
private byte memoizedIsInitialized = -1; private byte memoizedIsInitialized = -1;
public final boolean isInitialized() { public final boolean isInitialized() {
@ -6286,6 +6368,9 @@ public final class Protos {
if (((bitField0_ & 0x00000002) == 0x00000002)) { if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeMessage(2, initialPayment_); output.writeMessage(2, initialPayment_);
} }
if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeBytes(3, clientKey_);
}
getUnknownFields().writeTo(output); getUnknownFields().writeTo(output);
} }
@ -6303,6 +6388,10 @@ public final class Protos {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeMessageSize(2, initialPayment_); .computeMessageSize(2, initialPayment_);
} }
if (((bitField0_ & 0x00000004) == 0x00000004)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(3, clientKey_);
}
size += getUnknownFields().getSerializedSize(); size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size; memoizedSerializedSize = size;
return size; return size;
@ -6433,6 +6522,8 @@ public final class Protos {
initialPaymentBuilder_.clear(); initialPaymentBuilder_.clear();
} }
bitField0_ = (bitField0_ & ~0x00000002); bitField0_ = (bitField0_ & ~0x00000002);
clientKey_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000004);
return this; return this;
} }
@ -6473,6 +6564,10 @@ public final class Protos {
} else { } else {
result.initialPayment_ = initialPaymentBuilder_.build(); result.initialPayment_ = initialPaymentBuilder_.build();
} }
if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
to_bitField0_ |= 0x00000004;
}
result.clientKey_ = clientKey_;
result.bitField0_ = to_bitField0_; result.bitField0_ = to_bitField0_;
onBuilt(); onBuilt();
return result; return result;
@ -6495,6 +6590,9 @@ public final class Protos {
if (other.hasInitialPayment()) { if (other.hasInitialPayment()) {
mergeInitialPayment(other.getInitialPayment()); mergeInitialPayment(other.getInitialPayment());
} }
if (other.hasClientKey()) {
setClientKey(other.getClientKey());
}
this.mergeUnknownFields(other.getUnknownFields()); this.mergeUnknownFields(other.getUnknownFields());
return this; return this;
} }
@ -6540,11 +6638,17 @@ public final class Protos {
* *
* <pre> * <pre>
* The serialized bytes of the transaction in Satoshi format. * 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 * * 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 * 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 * * 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 * 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") * 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.
* </pre> * </pre>
*/ */
public boolean hasTx() { public boolean hasTx() {
@ -6555,11 +6659,17 @@ public final class Protos {
* *
* <pre> * <pre>
* The serialized bytes of the transaction in Satoshi format. * 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 * * 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 * 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 * * 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 * 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") * 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.
* </pre> * </pre>
*/ */
public com.google.protobuf.ByteString getTx() { public com.google.protobuf.ByteString getTx() {
@ -6570,11 +6680,17 @@ public final class Protos {
* *
* <pre> * <pre>
* The serialized bytes of the transaction in Satoshi format. * 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 * * 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 * 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 * * 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 * 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") * 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.
* </pre> * </pre>
*/ */
public Builder setTx(com.google.protobuf.ByteString value) { public Builder setTx(com.google.protobuf.ByteString value) {
@ -6591,11 +6707,17 @@ public final class Protos {
* *
* <pre> * <pre>
* The serialized bytes of the transaction in Satoshi format. * 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 * * 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 * 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 * * 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 * 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") * 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.
* </pre> * </pre>
*/ */
public Builder clearTx() { public Builder clearTx() {
@ -6784,6 +6906,69 @@ public final class Protos {
return initialPaymentBuilder_; return initialPaymentBuilder_;
} }
private com.google.protobuf.ByteString clientKey_ = com.google.protobuf.ByteString.EMPTY;
/**
* <code>optional bytes client_key = 3;</code>
*
* <pre>
* 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.
* </pre>
*/
public boolean hasClientKey() {
return ((bitField0_ & 0x00000004) == 0x00000004);
}
/**
* <code>optional bytes client_key = 3;</code>
*
* <pre>
* 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.
* </pre>
*/
public com.google.protobuf.ByteString getClientKey() {
return clientKey_;
}
/**
* <code>optional bytes client_key = 3;</code>
*
* <pre>
* 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.
* </pre>
*/
public Builder setClientKey(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000004;
clientKey_ = value;
onChanged();
return this;
}
/**
* <code>optional bytes client_key = 3;</code>
*
* <pre>
* 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.
* </pre>
*/
public Builder clearClientKey() {
bitField0_ = (bitField0_ & ~0x00000004);
clientKey_ = getDefaultInstance().getClientKey();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:paymentchannels.ProvideContract) // @@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" + "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" + "\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\"!", "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" + "videContract\022\n\n\002tx\030\001 \002(\014\0227\n\017initial_paym" +
"ent\030\002 \002(\0132\036.paymentchannels.UpdatePaymen" + "ent\030\002 \002(\0132\036.paymentchannels.UpdatePaymen" +
"t\"M\n\rUpdatePayment\022\033\n\023client_change_valu" + "t\022\022\n\nclient_key\030\003 \001(\014\"M\n\rUpdatePayment\022\033" +
"e\030\001 \002(\004\022\021\n\tsignature\030\002 \002(\014\022\014\n\004info\030\003 \001(\014" + "\n\023client_change_value\030\001 \002(\004\022\021\n\tsignature" +
"\"\032\n\nPaymentAck\022\014\n\004info\030\001 \001(\014\"\030\n\nSettleme" + "\030\002 \002(\014\022\014\n\004info\030\003 \001(\014\"\032\n\nPaymentAck\022\014\n\004in" +
"nt\022\n\n\002tx\030\003 \002(\014\"\251\002\n\005Error\0225\n\004code\030\001 \001(\0162 " + "fo\030\001 \001(\014\"\030\n\nSettlement\022\n\n\002tx\030\003 \002(\014\"\251\002\n\005E" +
".paymentchannels.Error.ErrorCode:\005OTHER\022" + "rror\0225\n\004code\030\001 \001(\0162 .paymentchannels.Err" +
"\023\n\013explanation\030\002 \001(\t\022\026\n\016expected_value\030\003" + "or.ErrorCode:\005OTHER\022\023\n\013explanation\030\002 \001(\t" +
" \001(\004\"\273\001\n\tErrorCode\022\013\n\007TIMEOUT\020\001\022\020\n\014SYNTA", "\022\026\n\016expected_value\030\003 \001(\004\"\273\001\n\tErrorCode\022\013",
"X_ERROR\020\002\022\031\n\025NO_ACCEPTABLE_VERSION\020\003\022\023\n\017" + "\n\007TIMEOUT\020\001\022\020\n\014SYNTAX_ERROR\020\002\022\031\n\025NO_ACCE" +
"BAD_TRANSACTION\020\004\022\034\n\030TIME_WINDOW_UNACCEP" + "PTABLE_VERSION\020\003\022\023\n\017BAD_TRANSACTION\020\004\022\034\n" +
"TABLE\020\005\022\033\n\027CHANNEL_VALUE_TOO_LARGE\020\006\022\031\n\025" + "\030TIME_WINDOW_UNACCEPTABLE\020\005\022\033\n\027CHANNEL_V" +
"MIN_PAYMENT_TOO_LARGE\020\007\022\t\n\005OTHER\020\010B$\n\032or" + "ALUE_TOO_LARGE\020\006\022\031\n\025MIN_PAYMENT_TOO_LARG" +
"g.bitcoin.paymentchannelB\006Protos" "E\020\007\022\t\n\005OTHER\020\010B$\n\032org.bitcoin.paymentcha" +
"nnelB\006Protos"
}; };
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() { new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() {
@ -9508,7 +9694,7 @@ public final class Protos {
internal_static_paymentchannels_ProvideContract_fieldAccessorTable = new internal_static_paymentchannels_ProvideContract_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable( com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_paymentchannels_ProvideContract_descriptor, internal_static_paymentchannels_ProvideContract_descriptor,
new java.lang.String[] { "Tx", "InitialPayment", }); new java.lang.String[] { "Tx", "InitialPayment", "ClientKey", });
internal_static_paymentchannels_UpdatePayment_descriptor = internal_static_paymentchannels_UpdatePayment_descriptor =
getDescriptor().getMessageTypes().get(7); getDescriptor().getMessageTypes().get(7);
internal_static_paymentchannels_UpdatePayment_fieldAccessorTable = new internal_static_paymentchannels_UpdatePayment_fieldAccessorTable = new

View File

@ -3818,11 +3818,12 @@ public class Wallet extends BaseTaggableObject
return toCLTVPaymentChannel(params, BigInteger.valueOf(time), from, to, value); return toCLTVPaymentChannel(params, BigInteger.valueOf(time), from, to, value);
} }
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, long lockTime, ECKey from, ECKey to, Coin value) { public static SendRequest toCLTVPaymentChannel(NetworkParameters params, int releaseBlock, ECKey from, ECKey to, Coin value) {
return toCLTVPaymentChannel(params, BigInteger.valueOf(lockTime), from, to, 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(); SendRequest req = new SendRequest();
Script output = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to); Script output = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to);
req.tx = new Transaction(params); 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); log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i);
continue; continue;
} catch (ScriptException e) { } catch (ScriptException e) {
log.debug("Input contained an incorrect signature", e);
// Expected. // Expected.
} }

View File

@ -772,10 +772,18 @@ public final class ClientState {
/** /**
* <code>required uint64 refundFees = 6;</code> * <code>required uint64 refundFees = 6;</code>
*
* <pre>
* Fees required to refund the transaction.
* </pre>
*/ */
boolean hasRefundFees(); boolean hasRefundFees();
/** /**
* <code>required uint64 refundFees = 6;</code> * <code>required uint64 refundFees = 6;</code>
*
* <pre>
* Fees required to refund the transaction.
* </pre>
*/ */
long getRefundFees(); long getRefundFees();
@ -799,6 +807,49 @@ public final class ClientState {
* </pre> * </pre>
*/ */
com.google.protobuf.ByteString getCloseTransactionHash(); com.google.protobuf.ByteString getCloseTransactionHash();
/**
* <code>optional uint32 majorVersion = 9 [default = 1];</code>
*/
boolean hasMajorVersion();
/**
* <code>optional uint32 majorVersion = 9 [default = 1];</code>
*/
int getMajorVersion();
/**
* <code>optional uint64 expiryTime = 10;</code>
*
* <pre>
* The expiry time of the CLTV lock. Only used in protocol v2.
* </pre>
*/
boolean hasExpiryTime();
/**
* <code>optional uint64 expiryTime = 10;</code>
*
* <pre>
* The expiry time of the CLTV lock. Only used in protocol v2.
* </pre>
*/
long getExpiryTime();
/**
* <code>optional bytes serverKey = 11;</code>
*
* <pre>
* The server's public key. Only used in protocol v2.
* </pre>
*/
boolean hasServerKey();
/**
* <code>optional bytes serverKey = 11;</code>
*
* <pre>
* The server's public key. Only used in protocol v2.
* </pre>
*/
com.google.protobuf.ByteString getServerKey();
} }
/** /**
* Protobuf type {@code paymentchannels.StoredClientPaymentChannel} * Protobuf type {@code paymentchannels.StoredClientPaymentChannel}
@ -897,6 +948,21 @@ public final class ClientState {
myPublicKey_ = input.readBytes(); myPublicKey_ = input.readBytes();
break; 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) { } catch (com.google.protobuf.InvalidProtocolBufferException e) {
@ -1039,12 +1105,20 @@ public final class ClientState {
private long refundFees_; private long refundFees_;
/** /**
* <code>required uint64 refundFees = 6;</code> * <code>required uint64 refundFees = 6;</code>
*
* <pre>
* Fees required to refund the transaction.
* </pre>
*/ */
public boolean hasRefundFees() { public boolean hasRefundFees() {
return ((bitField0_ & 0x00000040) == 0x00000040); return ((bitField0_ & 0x00000040) == 0x00000040);
} }
/** /**
* <code>required uint64 refundFees = 6;</code> * <code>required uint64 refundFees = 6;</code>
*
* <pre>
* Fees required to refund the transaction.
* </pre>
*/ */
public long getRefundFees() { public long getRefundFees() {
return refundFees_; return refundFees_;
@ -1077,6 +1151,67 @@ public final class ClientState {
return closeTransactionHash_; return closeTransactionHash_;
} }
public static final int MAJORVERSION_FIELD_NUMBER = 9;
private int majorVersion_;
/**
* <code>optional uint32 majorVersion = 9 [default = 1];</code>
*/
public boolean hasMajorVersion() {
return ((bitField0_ & 0x00000100) == 0x00000100);
}
/**
* <code>optional uint32 majorVersion = 9 [default = 1];</code>
*/
public int getMajorVersion() {
return majorVersion_;
}
public static final int EXPIRYTIME_FIELD_NUMBER = 10;
private long expiryTime_;
/**
* <code>optional uint64 expiryTime = 10;</code>
*
* <pre>
* The expiry time of the CLTV lock. Only used in protocol v2.
* </pre>
*/
public boolean hasExpiryTime() {
return ((bitField0_ & 0x00000200) == 0x00000200);
}
/**
* <code>optional uint64 expiryTime = 10;</code>
*
* <pre>
* The expiry time of the CLTV lock. Only used in protocol v2.
* </pre>
*/
public long getExpiryTime() {
return expiryTime_;
}
public static final int SERVERKEY_FIELD_NUMBER = 11;
private com.google.protobuf.ByteString serverKey_;
/**
* <code>optional bytes serverKey = 11;</code>
*
* <pre>
* The server's public key. Only used in protocol v2.
* </pre>
*/
public boolean hasServerKey() {
return ((bitField0_ & 0x00000400) == 0x00000400);
}
/**
* <code>optional bytes serverKey = 11;</code>
*
* <pre>
* The server's public key. Only used in protocol v2.
* </pre>
*/
public com.google.protobuf.ByteString getServerKey() {
return serverKey_;
}
private void initFields() { private void initFields() {
id_ = com.google.protobuf.ByteString.EMPTY; id_ = com.google.protobuf.ByteString.EMPTY;
contractTransaction_ = com.google.protobuf.ByteString.EMPTY; contractTransaction_ = com.google.protobuf.ByteString.EMPTY;
@ -1086,6 +1221,9 @@ public final class ClientState {
valueToMe_ = 0L; valueToMe_ = 0L;
refundFees_ = 0L; refundFees_ = 0L;
closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY; closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
majorVersion_ = 1;
expiryTime_ = 0L;
serverKey_ = com.google.protobuf.ByteString.EMPTY;
} }
private byte memoizedIsInitialized = -1; private byte memoizedIsInitialized = -1;
public final boolean isInitialized() { public final boolean isInitialized() {
@ -1152,6 +1290,15 @@ public final class ClientState {
if (((bitField0_ & 0x00000008) == 0x00000008)) { if (((bitField0_ & 0x00000008) == 0x00000008)) {
output.writeBytes(8, myPublicKey_); 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); getUnknownFields().writeTo(output);
} }
@ -1193,6 +1340,18 @@ public final class ClientState {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeBytesSize(8, myPublicKey_); .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(); size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size; memoizedSerializedSize = size;
return size; return size;
@ -1331,6 +1490,12 @@ public final class ClientState {
bitField0_ = (bitField0_ & ~0x00000040); bitField0_ = (bitField0_ & ~0x00000040);
closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY; closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000080); bitField0_ = (bitField0_ & ~0x00000080);
majorVersion_ = 1;
bitField0_ = (bitField0_ & ~0x00000100);
expiryTime_ = 0L;
bitField0_ = (bitField0_ & ~0x00000200);
serverKey_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000400);
return this; return this;
} }
@ -1391,6 +1556,18 @@ public final class ClientState {
to_bitField0_ |= 0x00000080; to_bitField0_ |= 0x00000080;
} }
result.closeTransactionHash_ = closeTransactionHash_; 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_; result.bitField0_ = to_bitField0_;
onBuilt(); onBuilt();
return result; return result;
@ -1431,6 +1608,15 @@ public final class ClientState {
if (other.hasCloseTransactionHash()) { if (other.hasCloseTransactionHash()) {
setCloseTransactionHash(other.getCloseTransactionHash()); 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()); this.mergeUnknownFields(other.getUnknownFields());
return this; return this;
} }
@ -1712,18 +1898,30 @@ public final class ClientState {
private long refundFees_ ; private long refundFees_ ;
/** /**
* <code>required uint64 refundFees = 6;</code> * <code>required uint64 refundFees = 6;</code>
*
* <pre>
* Fees required to refund the transaction.
* </pre>
*/ */
public boolean hasRefundFees() { public boolean hasRefundFees() {
return ((bitField0_ & 0x00000040) == 0x00000040); return ((bitField0_ & 0x00000040) == 0x00000040);
} }
/** /**
* <code>required uint64 refundFees = 6;</code> * <code>required uint64 refundFees = 6;</code>
*
* <pre>
* Fees required to refund the transaction.
* </pre>
*/ */
public long getRefundFees() { public long getRefundFees() {
return refundFees_; return refundFees_;
} }
/** /**
* <code>required uint64 refundFees = 6;</code> * <code>required uint64 refundFees = 6;</code>
*
* <pre>
* Fees required to refund the transaction.
* </pre>
*/ */
public Builder setRefundFees(long value) { public Builder setRefundFees(long value) {
bitField0_ |= 0x00000040; bitField0_ |= 0x00000040;
@ -1733,6 +1931,10 @@ public final class ClientState {
} }
/** /**
* <code>required uint64 refundFees = 6;</code> * <code>required uint64 refundFees = 6;</code>
*
* <pre>
* Fees required to refund the transaction.
* </pre>
*/ */
public Builder clearRefundFees() { public Builder clearRefundFees() {
bitField0_ = (bitField0_ & ~0x00000040); bitField0_ = (bitField0_ & ~0x00000040);
@ -1800,6 +2002,137 @@ public final class ClientState {
return this; return this;
} }
private int majorVersion_ = 1;
/**
* <code>optional uint32 majorVersion = 9 [default = 1];</code>
*/
public boolean hasMajorVersion() {
return ((bitField0_ & 0x00000100) == 0x00000100);
}
/**
* <code>optional uint32 majorVersion = 9 [default = 1];</code>
*/
public int getMajorVersion() {
return majorVersion_;
}
/**
* <code>optional uint32 majorVersion = 9 [default = 1];</code>
*/
public Builder setMajorVersion(int value) {
bitField0_ |= 0x00000100;
majorVersion_ = value;
onChanged();
return this;
}
/**
* <code>optional uint32 majorVersion = 9 [default = 1];</code>
*/
public Builder clearMajorVersion() {
bitField0_ = (bitField0_ & ~0x00000100);
majorVersion_ = 1;
onChanged();
return this;
}
private long expiryTime_ ;
/**
* <code>optional uint64 expiryTime = 10;</code>
*
* <pre>
* The expiry time of the CLTV lock. Only used in protocol v2.
* </pre>
*/
public boolean hasExpiryTime() {
return ((bitField0_ & 0x00000200) == 0x00000200);
}
/**
* <code>optional uint64 expiryTime = 10;</code>
*
* <pre>
* The expiry time of the CLTV lock. Only used in protocol v2.
* </pre>
*/
public long getExpiryTime() {
return expiryTime_;
}
/**
* <code>optional uint64 expiryTime = 10;</code>
*
* <pre>
* The expiry time of the CLTV lock. Only used in protocol v2.
* </pre>
*/
public Builder setExpiryTime(long value) {
bitField0_ |= 0x00000200;
expiryTime_ = value;
onChanged();
return this;
}
/**
* <code>optional uint64 expiryTime = 10;</code>
*
* <pre>
* The expiry time of the CLTV lock. Only used in protocol v2.
* </pre>
*/
public Builder clearExpiryTime() {
bitField0_ = (bitField0_ & ~0x00000200);
expiryTime_ = 0L;
onChanged();
return this;
}
private com.google.protobuf.ByteString serverKey_ = com.google.protobuf.ByteString.EMPTY;
/**
* <code>optional bytes serverKey = 11;</code>
*
* <pre>
* The server's public key. Only used in protocol v2.
* </pre>
*/
public boolean hasServerKey() {
return ((bitField0_ & 0x00000400) == 0x00000400);
}
/**
* <code>optional bytes serverKey = 11;</code>
*
* <pre>
* The server's public key. Only used in protocol v2.
* </pre>
*/
public com.google.protobuf.ByteString getServerKey() {
return serverKey_;
}
/**
* <code>optional bytes serverKey = 11;</code>
*
* <pre>
* The server's public key. Only used in protocol v2.
* </pre>
*/
public Builder setServerKey(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000400;
serverKey_ = value;
onChanged();
return this;
}
/**
* <code>optional bytes serverKey = 11;</code>
*
* <pre>
* The server's public key. Only used in protocol v2.
* </pre>
*/
public Builder clearServerKey() {
bitField0_ = (bitField0_ & ~0x00000400);
serverKey_ = getDefaultInstance().getServerKey();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:paymentchannels.StoredClientPaymentChannel) // @@protoc_insertion_point(builder_scope:paymentchannels.StoredClientPaymentChannel)
} }
@ -1833,13 +2166,15 @@ public final class ClientState {
"\n storedclientpaymentchannel.proto\022\017paym" + "\n storedclientpaymentchannel.proto\022\017paym" +
"entchannels\"\\\n\033StoredClientPaymentChanne" + "entchannels\"\\\n\033StoredClientPaymentChanne" +
"ls\022=\n\010channels\030\001 \003(\0132+.paymentchannels.S" + "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" + "ntPaymentChannel\022\n\n\002id\030\001 \002(\014\022\033\n\023contract" +
"Transaction\030\002 \002(\014\022\031\n\021refundTransaction\030\003" + "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" + " \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" + "\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" + "\034\n\024closeTransactionHash\030\007 \001(\014\022\027\n\014majorVe" +
"coinj.protocols.channelsB\013ClientState" "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 = com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() { new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() {
@ -1864,7 +2199,7 @@ public final class ClientState {
internal_static_paymentchannels_StoredClientPaymentChannel_fieldAccessorTable = new internal_static_paymentchannels_StoredClientPaymentChannel_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable( com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_paymentchannels_StoredClientPaymentChannel_descriptor, 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) // @@protoc_insertion_point(outer_class_scope)

View File

@ -52,12 +52,12 @@ import static com.google.common.base.Preconditions.checkState;
*/ */
public class PaymentChannelClient implements IPaymentChannelClient { public class PaymentChannelClient implements IPaymentChannelClient {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelClient.class); 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"); 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; @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 // 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; @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 // Will either hold the StoredClientChannel of this channel or null after connectionOpen
private StoredClientChannel storedChannel; private StoredClientChannel storedChannel;
// An arbitrary hash which identifies this channel (specified by the API user) // 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 * @param conn A callback listener which represents the connection to the server (forwards messages we generate to
* the server) * the server)
*/ */
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, ClientConnection conn) { public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId,
this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, null, conn); 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, public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, long timeWindow,
@Nullable KeyParameter userKeySetup, ClientConnection conn) { @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.wallet = checkNotNull(wallet);
this.myKey = checkNotNull(myKey); this.myKey = checkNotNull(myKey);
this.maxValue = checkNotNull(maxValue); this.maxValue = checkNotNull(maxValue);
@ -163,6 +256,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
this.timeWindow = timeWindow; this.timeWindow = timeWindow;
this.conn = checkNotNull(conn); this.conn = checkNotNull(conn);
this.userKeySetup = userKeySetup; this.userKeySetup = userKeySetup;
this.versionSelector = versionSelector;
} }
/** /**
@ -215,7 +309,16 @@ public class PaymentChannelClient implements IPaymentChannelClient {
final byte[] pubKeyBytes = initiate.getMultisigKey().toByteArray(); final byte[] pubKeyBytes = initiate.getMultisigKey().toByteArray();
if (!ECKey.isPubKeyCanonical(pubKeyBytes)) if (!ECKey.isPubKeyCanonical(pubKeyBytes))
throw new VerificationException("Server gave us a non-canonical public key, protocol error."); 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 { try {
state.initiate(userKeySetup); state.initiate(userKeySetup);
} catch (ValueOutOfRangeException e) { } catch (ValueOutOfRangeException e) {
@ -224,25 +327,62 @@ public class PaymentChannelClient implements IPaymentChannelClient {
return CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE; return CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE;
} }
minPayment = initiate.getMinPayment(); 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() Protos.ProvideRefund.Builder provideRefundBuilder = Protos.ProvideRefund.newBuilder()
.setMultisigKey(ByteString.copyFrom(myKey.getPubKey())) .setMultisigKey(ByteString.copyFrom(myKey.getPubKey()))
.setTx(ByteString.copyFrom(state.getIncompleteRefundTransaction().bitcoinSerialize())); .setTx(ByteString.copyFrom(((PaymentChannelV1ClientState)state).getIncompleteRefundTransaction().bitcoinSerialize()));
conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder() conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder()
.setProvideRefund(provideRefundBuilder) .setProvideRefund(provideRefundBuilder)
.setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_REFUND) .setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_REFUND)
.build()); .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; return null;
} }
@GuardedBy("lock") @GuardedBy("lock")
private void receiveRefund(Protos.TwoWayChannelMessage refundMsg, @Nullable KeyParameter userKey) throws VerificationException { private void receiveRefund(Protos.TwoWayChannelMessage refundMsg, @Nullable KeyParameter userKey) throws VerificationException {
checkState(majorVersion == 1);
checkState(step == InitStep.WAITING_FOR_REFUND_RETURN && refundMsg.hasReturnRefund()); checkState(step == InitStep.WAITING_FOR_REFUND_RETURN && refundMsg.hasReturnRefund());
log.info("Got RETURN_REFUND message, providing signed contract"); log.info("Got RETURN_REFUND message, providing signed contract");
Protos.ReturnRefund returnedRefund = refundMsg.getReturnRefund(); 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; 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 // 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); state.storeChannelInWallet(serverId);
Protos.ProvideContract.Builder contractMsg = Protos.ProvideContract.newBuilder() Protos.ProvideContract.Builder contractMsg = Protos.ProvideContract.newBuilder()
.setTx(ByteString.copyFrom(state.getMultisigContract().bitcoinSerialize())); .setTx(ByteString.copyFrom(state.getContract().bitcoinSerialize()));
try { try {
// Make an initial payment of the dust limit, and put it into the message as well. The size of the // 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. // 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) { if (step == InitStep.WAITING_FOR_INITIATE) {
// We skipped the initiate step, because a previous channel that's still valid was resumed. // We skipped the initiate step, because a previous channel that's still valid was resumed.
wasInitiated = false; 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; step = InitStep.CHANNEL_OPEN;
// channelOpen should disable timeouts, but // channelOpen should disable timeouts, but
@ -302,7 +451,8 @@ public class PaymentChannelClient implements IPaymentChannelClient {
checkState(step == InitStep.WAITING_FOR_VERSION_NEGOTIATION && msg.hasServerVersion()); 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 // 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. // 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() errorBuilder = Protos.Error.newBuilder()
.setCode(Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION); .setCode(Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION);
closeReason = CloseReason.NO_ACCEPTABLE_VERSION; closeReason = CloseReason.NO_ACCEPTABLE_VERSION;
@ -474,8 +624,8 @@ public class PaymentChannelClient implements IPaymentChannelClient {
step = InitStep.WAITING_FOR_VERSION_NEGOTIATION; step = InitStep.WAITING_FOR_VERSION_NEGOTIATION;
Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder() Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder()
.setMajor(CLIENT_MAJOR_VERSION) .setMajor(versionSelector.getRequestedMajorVersion())
.setMinor(CLIENT_MINOR_VERSION) .setMinor(versionSelector.getRequestedMinorVersion())
.setTimeWindowSecs(timeWindow); .setTimeWindowSecs(timeWindow);
if (storedChannel != null) { if (storedChannel != null) {
@ -557,7 +707,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
if (wallet.isEncrypted() && userKey == null) if (wallet.isEncrypted() && userKey == null)
throw new ECKey.KeyIsEncryptedException(); 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() Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder()
.setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin())) .setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin()))
.setClientChangeValue(state.getValueRefunded().value); .setClientChangeValue(state.getValueRefunded().value);

View File

@ -51,27 +51,86 @@ public class PaymentChannelClientConnection {
* {@link org.bitcoinj.protocols.channels.PaymentChannelClient#DEFAULT_TIME_WINDOW} * {@link org.bitcoinj.protocols.channels.PaymentChannelClient#DEFAULT_TIME_WINDOW}
* seconds. If the server proposes a longer time the channel will be closed. * 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 * @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough
* to accommodate ECDSA signature operations and network latency. * to accommodate ECDSA signature operations and network latency.
* @param wallet The wallet which will be paid from, and where completed transactions will be committed. * @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. * 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 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 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. * @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 * 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 * API, this should also probably encompass some caller UID to avoid applications opening channels
* which were created by others. * which were created by others.
* * @throws IOException if there's an issue using the network.
* @throws IOException if there's an issue using the network.
* @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue. * @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue.
*/ */
public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey, public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey,
Coin maxValue, String serverId) throws IOException, ValueOutOfRangeException { Coin maxValue, String serverId) throws IOException, ValueOutOfRangeException {
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId, 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 * 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} * 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. * which were created by others.
* @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open. * @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 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 IOException if there's an issue using the network.
* @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue. * @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue.
*/ */
public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey, public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey,
Coin maxValue, String serverId, final long timeWindow, Coin maxValue, String serverId, final long timeWindow,
@Nullable KeyParameter userKeySetup) @Nullable KeyParameter userKeySetup, PaymentChannelClient.VersionSelector versionSelector)
throws IOException, ValueOutOfRangeException { throws IOException, ValueOutOfRangeException {
// Glue the object which vends/ingests protobuf messages in order to manage state to the network object which // 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. // 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. // Inform the API user that we're done and ready to roll.
channelOpenFuture.set(PaymentChannelClientConnection.this); channelOpenFuture.set(PaymentChannelClientConnection.this);
} }
}); }, versionSelector);
// And glue back in the opposite direction - network to the channelClient. // And glue back in the opposite direction - network to the channelClient.
wireParser = new ProtobufConnection<Protos.TwoWayChannelMessage>(new ProtobufConnection.Listener<Protos.TwoWayChannelMessage>() { wireParser = new ProtobufConnection<Protos.TwoWayChannelMessage>(new ProtobufConnection.Listener<Protos.TwoWayChannelMessage>() {
@ -217,7 +280,7 @@ public class PaymentChannelClientConnection {
} }
/** /**
* <p>Gets the {@link PaymentChannelClientState} object which stores the current state of the connection with the * <p>Gets the {@link PaymentChannelV1ClientState} object which stores the current state of the connection with the
* server.</p> * server.</p>
* *
* <p>Note that if you call any methods which update state directly the server will not be notified and channel * <p>Note that if you call any methods which update state directly the server will not be notified and channel

View File

@ -16,24 +16,22 @@
package org.bitcoinj.protocols.channels; 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.annotations.VisibleForTesting;
import com.google.common.base.Throwables; 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.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.List;
import static com.google.common.base.Preconditions.*; import static com.google.common.base.Preconditions.*;
import org.bitcoinj.core.listeners.WalletCoinsReceivedEventListener; 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 * 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.</p> * negotiation is. Note that this class only allows the amount of money sent to be incremented, not decremented.</p>
* *
* <p>This class has two subclasses, {@link PaymentChannelV1ClientState} and {@link PaymentChannelV2ClientState} for
* protocols version 1 and 2.</p>
*
* <p>This class implements the core state machine for the client side of the protocol. The server side is implemented * <p>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 * 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 * 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 * 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.</p> * transaction and take back all the money once the expiry time is reached.</p>
* *
* <p>To begin, the client calls {@link PaymentChannelClientState#initiate()}, which moves the channel into state * <p>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 * 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 * exception will be thrown at this point. Once this is done, call
* {@link PaymentChannelClientState#getIncompleteRefundTransaction()} and pass the resultant transaction through to the * {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction()} and pass the resultant transaction through to the
* server. Once you have retrieved the signature, use {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)}. * server. Once you have retrieved the signature, use {@link PaymentChannelV1ClientState#provideRefundSignature(byte[], KeyParameter)}.
* You must then call {@link PaymentChannelClientState#storeChannelInWallet(Sha256Hash)} to store the refund transaction * 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 * 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.
* </p> * </p>
*/ */
public class PaymentChannelClientState { public abstract class PaymentChannelClientState {
private static final Logger log = LoggerFactory.getLogger(PaymentChannelClientState.class); 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. // 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 * 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. * by the time the NEW state is reached.
*/ */
public enum State { public enum State {
UNINITIALISED,
NEW, NEW,
INITIATED, INITIATED,
WAITING_FOR_SIGNED_REFUND, WAITING_FOR_SIGNED_REFUND,
@ -106,26 +90,24 @@ public class PaymentChannelClientState {
EXPIRED, EXPIRED,
CLOSED CLOSED
} }
private State state; protected final StateMachine<State> 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 // 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 { PaymentChannelClientState(StoredClientChannel storedClientChannel, Wallet wallet) throws VerificationException {
// The PaymentChannelClientConnection handles storedClientChannel.active and ensures we aren't resuming channels this.stateMachine = new StateMachine<State>(State.UNINITIALISED, getStateTransitions());
this.wallet = checkNotNull(wallet); 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.myKey = checkNotNull(storedClientChannel.myKey);
this.serverMultisigKey = null; this.serverKey = checkNotNull(storedClientChannel.serverKey);
this.totalValue = multisigContract.getOutput(0).getValue();
this.valueToMe = checkNotNull(storedClientChannel.valueToMe);
this.storedChannel = storedClientChannel; this.storedChannel = storedClientChannel;
this.state = State.READY; this.valueToMe = checkNotNull(storedClientChannel.valueToMe);
initWalletListeners();
} }
/** /**
@ -134,7 +116,7 @@ public class PaymentChannelClientState {
public synchronized boolean isSettlementTransaction(Transaction tx) { public synchronized boolean isSettlementTransaction(Transaction tx) {
try { try {
tx.verify(); tx.verify();
tx.getInput(0).verify(multisigContract.getOutput(0)); tx.getInput(0).verify(getContractInternal().getOutput(0));
return true; return true;
} catch (VerificationException e) { } catch (VerificationException e) {
return false; 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 * 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 * 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 wallet a wallet that contains at least the specified amount of value.
* @param myKey a freshly generated private key for this channel. * @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. * @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 * 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 * @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 { Coin value, long expiryTimeInSeconds) throws VerificationException {
checkArgument(value.signum() > 0); this.stateMachine = new StateMachine<State>(State.UNINITIALISED, getStateTransitions());
this.wallet = checkNotNull(wallet); this.wallet = checkNotNull(wallet);
initWalletListeners(); this.serverKey = checkNotNull(serverKey);
this.serverMultisigKey = checkNotNull(serverMultisigKey);
this.myKey = checkNotNull(myKey); this.myKey = checkNotNull(myKey);
this.valueToMe = this.totalValue = checkNotNull(value); this.valueToMe = checkNotNull(value);
this.expiryTime = expiryTimeInSeconds;
this.state = State.NEW;
} }
private synchronized void initWalletListeners() { protected synchronized void initWalletListeners() {
// Register a listener that watches out for the server closing the channel. // Register a listener that watches out for the server closing the channel.
if (storedChannel != null && storedChannel.close != null) { if (storedChannel != null && storedChannel.close != null) {
watchCloseConfirmations(); watchCloseConfirmations();
@ -177,11 +156,11 @@ public class PaymentChannelClientState {
@Override @Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
synchronized (PaymentChannelClientState.this) { synchronized (PaymentChannelClientState.this) {
if (multisigContract == null) return; if (getContractInternal() == null) return;
if (isSettlementTransaction(tx)) { 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. // 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; if (storedChannel == null) return;
storedChannel.close = tx; storedChannel.close = tx;
updateChannelInWallet(); 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 // 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 // of this channel along with the refund tx from the wallet, because we're not going to need
// any of that any more. // any of that any more.
@ -220,18 +199,19 @@ public class PaymentChannelClientState {
storedChannel = null; storedChannel = null;
} }
/**
* This object implements a state machine, and this accessor returns which state it's currently in.
*/
public synchronized State getState() { public synchronized State getState() {
return state; return stateMachine.getState();
} }
protected abstract Multimap<State, State> getStateTransitions();
public abstract int getMajorVersion();
/** /**
* Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate * Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate
* time using {@link PaymentChannelClientState#getIncompleteRefundTransaction} and * time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and
* {@link PaymentChannelClientState#getMultisigContract()}. The way the contract is crafted can be adjusted by * {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by
* overriding {@link PaymentChannelClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}. * 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. * 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 * @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 * Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate
* time using {@link PaymentChannelClientState#getIncompleteRefundTransaction} and * time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and
* {@link PaymentChannelClientState#getMultisigContract()}. The way the contract is crafted can be adjusted by * {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by
* overriding {@link PaymentChannelClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}. * 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. * 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. * @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted.
* The wallet KeyCrypter is assumed. * 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 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 * @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate
*/ */
public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException { public abstract 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<ECKey> 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.
}
/** /**
* You can override this method in order to control the construction of the initial contract that creates the * 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. * Gets the contract which was used to initialize this channel
* Once this step is done, you can use {@link PaymentChannelClientState#incrementPaymentBy(Coin, KeyParameter)} to
* start paying the server.
*/ */
public synchronized Transaction getMultisigContract() { public abstract Transaction getContract();
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;
}
/**
* <p>When the servers signature for the refund transaction is received, call this to verify it and sign the
* complete refund ourselves.</p>
*
* <p>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.</p>
*/
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;
}
private synchronized Transaction makeUnsignedChannelContract(Coin valueToMe) throws ValueOutOfRangeException { private synchronized Transaction makeUnsignedChannelContract(Coin valueToMe) throws ValueOutOfRangeException {
Transaction tx = new Transaction(wallet.getParams()); Transaction tx = new Transaction(wallet.getParams());
tx.addInput(multisigContract.getOutput(0)); tx.addInput(getContractInternal().getOutput(0));
// Our output always comes first. // Our output always comes first.
// TODO: We should drop myKey in favor of output key + multisig key separation // 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) // (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. * storage and throwing an {@link IllegalStateException} if it is.
*/ */
public synchronized void checkNotExpired() { public synchronized void checkNotExpired() {
if (Utils.currentTimeSeconds() > expiryTime) { if (Utils.currentTimeSeconds() > getExpiryTime()) {
state = State.EXPIRED; stateMachine.transition(State.EXPIRED);
disconnectFromChannel(); disconnectFromChannel();
throw new IllegalStateException("Channel expired"); throw new IllegalStateException("Channel expired");
} }
@ -404,8 +284,8 @@ public class PaymentChannelClientState {
* <p>The returned signature is over the payment transaction, which we never have a valid copy of and thus there * <p>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.</p> * is no accessor for it on this object.</p>
* *
* <p>To spend the whole channel increment by {@link PaymentChannelClientState#getTotalValue()} - * <p>To spend the whole channel increment by {@link PaymentChannelV1ClientState#getTotalValue()} -
* {@link PaymentChannelClientState#getValueRefunded()}</p> * {@link PaymentChannelV1ClientState#getValueRefunded()}</p>
* *
* @param size How many satoshis to increment the payment by (note: not the new total). * @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 * @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) public synchronized IncrementedPayment incrementPaymentBy(Coin size, @Nullable KeyParameter userKey)
throws ValueOutOfRangeException { throws ValueOutOfRangeException {
checkState(state == State.READY); stateMachine.checkState(State.READY);
checkNotExpired(); checkNotExpired();
checkNotNull(size); // Validity of size will be checked by makeUnsignedChannelContract. checkNotNull(size); // Validity of size will be checked by makeUnsignedChannelContract.
if (size.signum() < 0) if (size.signum() < 0)
throw new ValueOutOfRangeException("Tried to decrement payment"); 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) { 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"); log.info("New value being sent back as change was smaller than minimum nondust output, sending all");
size = valueToMe; size = getValueToMe();
newValueToMe = Coin.ZERO; newValueToMe = Coin.ZERO;
} }
if (newValueToMe.signum() < 0) if (newValueToMe.signum() < 0)
@ -435,7 +315,7 @@ public class PaymentChannelClientState {
mode = Transaction.SigHash.NONE; mode = Transaction.SigHash.NONE;
else else
mode = Transaction.SigHash.SINGLE; 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; valueToMe = newValueToMe;
updateChannelInWallet(); updateChannelInWallet();
IncrementedPayment payment = new IncrementedPayment(); IncrementedPayment payment = new IncrementedPayment();
@ -444,10 +324,10 @@ public class PaymentChannelClientState {
return payment; return payment;
} }
private synchronized void updateChannelInWallet() { protected synchronized void updateChannelInWallet() {
if (storedChannel == null) if (storedChannel == null)
return; return;
storedChannel.valueToMe = valueToMe; storedChannel.valueToMe = getValueToMe();
StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates) StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates)
wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
channels.updatedChannel(storedChannel); 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 * Sets this channel's state in {@link StoredPaymentChannelClientStates} to unopened so this channel can be reopened
* later. * later.
* *
* @see PaymentChannelClientState#storeChannelInWallet(Sha256Hash) * @see PaymentChannelV1ClientState#storeChannelInWallet(Sha256Hash)
*/ */
public synchronized void disconnectFromChannel() { public synchronized void disconnectFromChannel() {
if (storedChannel == null) if (storedChannel == null)
@ -472,21 +352,14 @@ public class PaymentChannelClientState {
*/ */
@VisibleForTesting synchronized void fakeSave() { @VisibleForTesting synchronized void fakeSave() {
try { try {
wallet.commitTx(multisigContract); wallet.commitTx(getContractInternal());
} catch (VerificationException e) { } catch (VerificationException e) {
throw new RuntimeException(e); // We created it 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) { @VisibleForTesting abstract 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);
}
/** /**
* <p>Stores this channel's state in the wallet as a part of a {@link StoredPaymentChannelClientStates} wallet * <p>Stores this channel's state in the wallet as a part of a {@link StoredPaymentChannelClientStates} wallet
@ -501,7 +374,8 @@ public class PaymentChannelClientState {
* unique. * unique.
*/ */
public synchronized void storeChannelInWallet(Sha256Hash id) { 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) { if (storedChannel != null) {
checkState(storedChannel.id.equals(id)); checkState(storedChannel.id.equals(id));
return; return;
@ -509,44 +383,31 @@ public class PaymentChannelClientState {
doStoreChannelInWallet(id); doStoreChannelInWallet(id);
try { try {
wallet.commitTx(multisigContract); wallet.commitTx(getContractInternal());
} catch (VerificationException e) { } catch (VerificationException e) {
throw new RuntimeException(e); // We created it 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 * 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() { public abstract Coin getRefundTxFees();
checkState(state.compareTo(State.NEW) > 0);
return refundFees;
}
/** @VisibleForTesting abstract Transaction getRefundTransaction();
* 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;
}
/** /**
* Gets the total value of this channel (ie the maximum payment possible) * Gets the total value of this channel (ie the maximum payment possible)
*/ */
public Coin getTotalValue() { public abstract Coin getTotalValue();
return totalValue;
}
/** /**
* Gets the current amount refunded to us from the multisig contract (ie totalValue-valueSentToServer) * Gets the current amount refunded to us from the multisig contract (ie totalValue-valueSentToServer)
*/ */
public synchronized Coin getValueRefunded() { public synchronized Coin getValueRefunded() {
checkState(state == State.READY); stateMachine.checkState(State.READY);
return valueToMe; return valueToMe;
} }
@ -556,4 +417,23 @@ public class PaymentChannelClientState {
public synchronized Coin getValueSpent() { public synchronized Coin getValueSpent() {
return getTotalValue().subtract(getValueRefunded()); 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();
} }

View File

@ -16,6 +16,7 @@
package org.bitcoinj.protocols.channels; package org.bitcoinj.protocols.channels;
import com.google.common.collect.ImmutableMap;
import org.bitcoinj.core.*; import org.bitcoinj.core.*;
import org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason; import org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason;
import org.bitcoinj.utils.Threading; import org.bitcoinj.utils.Threading;
@ -28,6 +29,7 @@ import org.bitcoin.paymentchannel.Protos;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.checkNotNull; 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); private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelServer.class);
protected final ReentrantLock lock = Threading.lock("channelserver"); 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<Integer, Integer> 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 // The step in the initialization process we are in, some of this is duplicated in the PaymentChannelServerState
private enum InitStep { private enum InitStep {
WAITING_ON_CLIENT_VERSION, WAITING_ON_CLIENT_VERSION,
// This step is only used in V1 of the protocol.
WAITING_ON_UNSIGNED_REFUND, WAITING_ON_UNSIGNED_REFUND,
WAITING_ON_CONTRACT, WAITING_ON_CONTRACT,
WAITING_ON_MULTISIG_ACCEPTANCE, WAITING_ON_MULTISIG_ACCEPTANCE,
@ -111,6 +118,9 @@ public class PaymentChannelServer {
} }
private final ServerConnection conn; 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 // 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; @GuardedBy("lock") private boolean connectionOpen = false;
// Indicates that no further messages should be sent and we intend to settle the connection // 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 { private void receiveVersionMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
checkState(step == InitStep.WAITING_ON_CLIENT_VERSION && msg.hasClientVersion()); checkState(step == InitStep.WAITING_ON_CLIENT_VERSION && msg.hasClientVersion());
final Protos.ClientVersion clientVersion = msg.getClientVersion(); final Protos.ClientVersion clientVersion = msg.getClientVersion();
final int major = clientVersion.getMajor(); majorVersion = clientVersion.getMajor();
if (major != SERVER_MAJOR_VERSION) { if (!SERVER_VERSIONS.containsKey(majorVersion)) {
error("This server needs protocol version " + SERVER_MAJOR_VERSION + " , client offered " + major, error("This server needs one of protocol versions " + SERVER_VERSIONS.keySet() + " , client offered " + majorVersion,
Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION, CloseReason.NO_ACCEPTABLE_VERSION); Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION, CloseReason.NO_ACCEPTABLE_VERSION);
return; return;
} }
Protos.ServerVersion.Builder versionNegotiationBuilder = Protos.ServerVersion.newBuilder() 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() conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder()
.setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION) .setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION)
.setServerVersion(versionNegotiationBuilder) .setServerVersion(versionNegotiationBuilder)
@ -262,7 +272,17 @@ public class PaymentChannelServer {
wallet.freshReceiveKey(); wallet.freshReceiveKey();
expireTime = Utils.currentTimeSeconds() + truncateTimeWindow(clientVersion.getTimeWindowSecs()); 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() Protos.Initiate.Builder initiateBuilder = Protos.Initiate.newBuilder()
.setMultisigKey(ByteString.copyFrom(myKey.getPubKey())) .setMultisigKey(ByteString.copyFrom(myKey.getPubKey()))
@ -290,13 +310,16 @@ public class PaymentChannelServer {
@GuardedBy("lock") @GuardedBy("lock")
private void receiveRefundMessage(Protos.TwoWayChannelMessage msg) throws VerificationException { private void receiveRefundMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
checkState(majorVersion == 1);
checkState(step == InitStep.WAITING_ON_UNSIGNED_REFUND && msg.hasProvideRefund()); checkState(step == InitStep.WAITING_ON_UNSIGNED_REFUND && msg.hasProvideRefund());
log.info("Got refund transaction, returning signature"); log.info("Got refund transaction, returning signature");
Protos.ProvideRefund providedRefund = msg.getProvideRefund(); Protos.ProvideRefund providedRefund = msg.getProvideRefund();
state = new PaymentChannelServerState(broadcaster, wallet, myKey, expireTime); state = new PaymentChannelV1ServerState(broadcaster, wallet, myKey, expireTime);
byte[] signature = state.provideRefundTransaction(wallet.getParams().getDefaultSerializer().makeTransaction(providedRefund.getTx().toByteArray()), // We can cast to V1 state since this state is only used in the V1 protocol
providedRefund.getMultisigKey().toByteArray()); byte[] signature = ((PaymentChannelV1ServerState) state)
.provideRefundTransaction(wallet.getParams().getDefaultSerializer().makeTransaction(providedRefund.getTx().toByteArray()),
providedRefund.getMultisigKey().toByteArray());
step = InitStep.WAITING_ON_CONTRACT; step = InitStep.WAITING_ON_CONTRACT;
@ -344,18 +367,25 @@ public class PaymentChannelServer {
@GuardedBy("lock") @GuardedBy("lock")
private void receiveContractMessage(Protos.TwoWayChannelMessage msg) throws VerificationException { private void receiveContractMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
checkState(majorVersion == 1 || majorVersion == 2);
checkState(step == InitStep.WAITING_ON_CONTRACT && msg.hasProvideContract()); checkState(step == InitStep.WAITING_ON_CONTRACT && msg.hasProvideContract());
log.info("Got contract, broadcasting and responding with CHANNEL_OPEN"); log.info("Got contract, broadcasting and responding with CHANNEL_OPEN");
final Protos.ProvideContract providedContract = msg.getProvideContract(); 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? //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; step = InitStep.WAITING_ON_MULTISIG_ACCEPTANCE;
state.provideMultiSigContract(multisigContract) state.provideContract(contract)
.addListener(new Runnable() { .addListener(new Runnable() {
@Override @Override
public void run() { public void run() {
multisigContractPropogated(providedContract, multisigContract.getHash()); multisigContractPropogated(providedContract, contract.getHash());
} }
}, Threading.SAME_THREAD); }, Threading.SAME_THREAD);
} }
@ -413,9 +443,6 @@ public class PaymentChannelServer {
checkState(connectionOpen); checkState(connectionOpen);
if (channelSettling) if (channelSettling)
return; return;
// If we generate an error, we set errorBuilder and closeReason and break, otherwise we return
Protos.Error.Builder errorBuilder;
CloseReason closeReason;
try { try {
switch (msg.getType()) { switch (msg.getType()) {
case CLIENT_VERSION: case CLIENT_VERSION:
@ -532,18 +559,18 @@ public class PaymentChannelServer {
connectionOpen = false; connectionOpen = false;
try { try {
if (state != null && state.getMultisigContract() != null) { if (state != null && state.getContract() != null) {
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)
wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID); wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID);
if (channels != null) { if (channels != null) {
StoredServerChannel storedServerChannel = channels.getChannel(state.getMultisigContract().getHash()); StoredServerChannel storedServerChannel = channels.getChannel(state.getContract().getHash());
if (storedServerChannel != null) { if (storedServerChannel != null) {
storedServerChannel.clearConnectedHandler(); storedServerChannel.clearConnectedHandler();
} }
} }
} }
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
// Expected when we call getMultisigContract() sometimes // Expected when we call getContract() sometimes
} }
} finally { } finally {
lock.unlock(); lock.unlock();

View File

@ -16,24 +16,27 @@
package org.bitcoinj.protocols.channels; 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.ImmutableList;
import com.google.common.collect.Lists; 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.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nullable; 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;
/** /**
* <p>A payment channel is a method of sending money to someone such that the amount of money you send can be adjusted * <p>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 * 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.</p> * negotiation is. Note that this class only allows the amount of money received to be incremented, not decremented.</p>
* *
* <p>There are two subclasses that implement this one, for versions 1 and 2 of the protocol -
* {@link PaymentChannelV1ServerState} and {@link PaymentChannelV2ServerState}.</p>
*
* <p>This class implements the core state machine for the server side of the protocol. The client side is implemented * <p>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 * protocol listening for TCP/IP connections and moving this class through each state. We say that the party who is
* sending funds is the <i>client</i> or <i>initiating party</i>. The party that is receiving the funds is the * sending funds is the <i>client</i> or <i>initiating party</i>. The party that is receiving the funds is the
* <i>server</i> or <i>receiving party</i>. Although the underlying Bitcoin protocol is capable of more complex * <i>server</i> or <i>receiving party</i>. 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 * 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.</p> * client, and the rest is ours to do with as we wish.</p>
*/ */
public class PaymentChannelServerState { public abstract class PaymentChannelServerState {
private static final Logger log = LoggerFactory.getLogger(PaymentChannelServerState.class); 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. * until READY, at which time the client can increase payment incrementally.
*/ */
public enum State { public enum State {
UNINITIALISED,
WAITING_FOR_REFUND_TRANSACTION, WAITING_FOR_REFUND_TRANSACTION,
WAITING_FOR_MULTISIG_CONTRACT, WAITING_FOR_MULTISIG_CONTRACT,
WAITING_FOR_MULTISIG_ACCEPTANCE, WAITING_FOR_MULTISIG_ACCEPTANCE,
@ -81,55 +88,43 @@ public class PaymentChannelServerState {
CLOSED, CLOSED,
ERROR, ERROR,
} }
private State state;
// The client and server keys for the multi-sig contract protected StateMachine<State> stateMachine;
// We currently also use the serverKey for payouts, but this is not required
private ECKey clientKey, serverKey;
// Package-local for checkArguments in StoredServerChannel // Package-local for checkArguments in StoredServerChannel
final Wallet wallet; final Wallet wallet;
// The object that will broadcast transactions for us - usually a peer group. // The object that will broadcast transactions for us - usually a peer group.
private final TransactionBroadcaster broadcaster; protected final TransactionBroadcaster broadcaster;
// The multi-sig contract and the output script from it
private Transaction multisigContract = null;
private Script multisigScript;
// The last signature the client provided for a payment transaction. // 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 protected Coin bestValueToMe = Coin.ZERO;
private Coin totalValue;
private Coin bestValueToMe = Coin.ZERO;
private Coin feePaidForPayment;
// The refund/change transaction output that goes back to the client // The server key for the multi-sig contract
private TransactionOutput clientOutput; // We currently also use the serverKey for payouts, but this is not required
private long refundTransactionUnlockTimeSecs; 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 { PaymentChannelServerState(StoredServerChannel storedServerChannel, Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException {
synchronized (storedServerChannel) { synchronized (storedServerChannel) {
this.stateMachine = new StateMachine<State>(State.UNINITIALISED, getStateTransitions());
this.wallet = checkNotNull(wallet); this.wallet = checkNotNull(wallet);
this.broadcaster = checkNotNull(broadcaster); this.broadcaster = checkNotNull(broadcaster);
this.multisigContract = checkNotNull(storedServerChannel.contract); this.contract = 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.serverKey = checkNotNull(storedServerChannel.myKey); this.serverKey = checkNotNull(storedServerChannel.myKey);
this.totalValue = multisigContract.getOutput(0).getValue(); this.storedServerChannel = storedServerChannel;
this.bestValueToMe = checkNotNull(storedServerChannel.bestValueToMe); this.bestValueToMe = checkNotNull(storedServerChannel.bestValueToMe);
this.bestValueSignature = storedServerChannel.bestValueSignature; this.bestValueSignature = storedServerChannel.bestValueSignature;
checkArgument(bestValueToMe.equals(Coin.ZERO) || bestValueSignature != null); checkArgument(bestValueToMe.equals(Coin.ZERO) || bestValueSignature != null);
this.storedServerChannel = storedServerChannel;
storedServerChannel.state = this; 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) * @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) { public PaymentChannelServerState(TransactionBroadcaster broadcaster, Wallet wallet, ECKey serverKey, long minExpireTime) {
this.state = State.WAITING_FOR_REFUND_TRANSACTION; this.stateMachine = new StateMachine<State>(State.UNINITIALISED, getStateTransitions());
this.serverKey = checkNotNull(serverKey); this.serverKey = checkNotNull(serverKey);
this.wallet = checkNotNull(wallet); this.wallet = checkNotNull(wallet);
this.broadcaster = checkNotNull(broadcaster); this.broadcaster = checkNotNull(broadcaster);
this.minExpireTime = minExpireTime; this.minExpireTime = minExpireTime;
} }
/** public abstract int getMajorVersion();
* This object implements a state machine, and this accessor returns which state it's currently in.
*/
public synchronized State getState() { public synchronized State getState() {
return state; return stateMachine.getState();
} }
/** protected abstract Multimap<State, State> getStateTransitions();
* 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();
}
/** /**
* Called when the client provides the multi-sig contract. Checks that the previously-provided refund transaction * 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 * 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). * 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 * @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. * 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 * @throws VerificationException If the provided multisig contract is not well-formed or does not meet previously-specified parameters
*/ */
public synchronized ListenableFuture<PaymentChannelServerState> provideMultiSigContract(final Transaction multisigContract) throws VerificationException { public synchronized ListenableFuture<PaymentChannelServerState> provideContract(final Transaction contract) throws VerificationException {
checkNotNull(multisigContract); checkNotNull(contract);
checkState(state == State.WAITING_FOR_MULTISIG_CONTRACT); stateMachine.checkState(State.WAITING_FOR_MULTISIG_CONTRACT);
try { try {
multisigContract.verify(); contract.verify();
this.multisigContract = multisigContract; this.contract = contract;
this.multisigScript = multisigContract.getOutput(0).getScriptPubKey(); verifyContract(contract);
// Check that multisigContract's first output is a 2-of-2 multisig to the correct pubkeys in the correct order // Check that contract'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)); final Script expectedScript = createOutputScript();
if (!Arrays.equals(multisigScript.getProgram(), expectedScript.getProgram())) if (!Arrays.equals(getContractScript().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."); 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 (getTotalValue().signum() <= 0)
if (this.totalValue.signum() <= 0)
throw new VerificationException("Not accepting an attempt to open a contract with zero value."); throw new VerificationException("Not accepting an attempt to open a contract with zero value.");
} catch (VerificationException e) { } catch (VerificationException e) {
// We couldn't parse the multisig transaction or its output. // 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; throw e;
} }
log.info("Broadcasting multisig contract: {}", multisigContract); log.info("Broadcasting multisig contract: {}", contract);
wallet.addWatchedScripts(ImmutableList.of(multisigContract.getOutput(0).getScriptPubKey())); wallet.addWatchedScripts(ImmutableList.of(contract.getOutput(0).getScriptPubKey()));
state = State.WAITING_FOR_MULTISIG_ACCEPTANCE; stateMachine.transition(State.WAITING_FOR_MULTISIG_ACCEPTANCE);
final SettableFuture<PaymentChannelServerState> future = SettableFuture.create(); final SettableFuture<PaymentChannelServerState> future = SettableFuture.create();
Futures.addCallback(broadcaster.broadcastTransaction(multisigContract).future(), new FutureCallback<Transaction>() { Futures.addCallback(broadcaster.broadcastTransaction(contract).future(), new FutureCallback<Transaction>() {
@Override public void onSuccess(Transaction transaction) { @Override public void onSuccess(Transaction transaction) {
log.info("Successfully broadcast multisig contract {}. Channel now open.", transaction.getHashAsString()); log.info("Successfully broadcast multisig contract {}. Channel now open.", transaction.getHashAsString());
try { 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 // it and check for double-spends later
wallet.receivePending(multisigContract, null, true); wallet.receivePending(contract, null, true);
} catch (VerificationException e) { } 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); future.set(PaymentChannelServerState.this);
} }
@Override public void onFailure(Throwable throwable) { @Override public void onFailure(Throwable throwable) {
// Couldn't broadcast the transaction for some reason. // Couldn't broadcast the transaction for some reason.
log.error("Broadcast multisig contract failed", throwable); log.error("Failed to broadcast contract", throwable);
state = State.ERROR; stateMachine.transition(State.ERROR);
future.setException(throwable); future.setException(throwable);
} }
}); });
@ -263,18 +214,17 @@ public class PaymentChannelServerState {
} }
// Create a payment transaction with valueToMe going back to us // 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()); Transaction tx = new Transaction(wallet.getParams());
if (!totalValue.subtract(valueToMe).equals(Coin.ZERO)) { if (!getTotalValue().subtract(valueToMe).equals(Coin.ZERO)) {
clientOutput.setValue(totalValue.subtract(valueToMe)); tx.addOutput(getTotalValue().subtract(valueToMe), getClientKey().toAddress(wallet.getParams()));
tx.addOutput(clientOutput);
} }
tx.addInput(multisigContract.getOutput(0)); tx.addInput(contract.getOutput(0));
return Wallet.SendRequest.forTx(tx); 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. * 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. * 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. * @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 { public synchronized boolean incrementPayment(Coin refundSize, byte[] signatureBytes) throws VerificationException, ValueOutOfRangeException, InsufficientMoneyException {
checkState(state == State.READY); stateMachine.checkState(State.READY);
checkNotNull(refundSize); checkNotNull(refundSize);
checkNotNull(signatureBytes); checkNotNull(signatureBytes);
TransactionSignature signature = TransactionSignature.decodeFromBitcoin(signatureBytes, true); 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 // 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. // the dust level because that would prevent the transaction from being relayed/mined.
final boolean fullyUsedUp = refundSize.equals(Coin.ZERO); final boolean fullyUsedUp = refundSize.equals(Coin.ZERO);
if (refundSize.compareTo(clientOutput.getMinNonDustValue()) < 0 && !fullyUsedUp) Coin newValueToMe = getTotalValue().subtract(refundSize);
throw new ValueOutOfRangeException("Attempt to refund negative value or value too small to be accepted by the network");
Coin newValueToMe = totalValue.subtract(refundSize);
if (newValueToMe.signum() < 0) if (newValueToMe.signum() < 0)
throw new ValueOutOfRangeException("Attempt to refund more than the contract allows."); throw new ValueOutOfRangeException("Attempt to refund more than the contract allows.");
if (newValueToMe.compareTo(bestValueToMe) < 0) if (newValueToMe.compareTo(bestValueToMe) < 0)
throw new ValueOutOfRangeException("Attempt to roll back payment on the channel."); 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 // 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) // disables our double-spend check next)
Transaction walletContract = wallet.getTransaction(multisigContract.getHash()); Transaction walletContract = wallet.getTransaction(contract.getHash());
checkNotNull(walletContract, "Wallet did not contain multisig contract {} after state was marked READY", multisigContract.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 // 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 // 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()) if (signature.sigHashMode() != mode || !signature.anyoneCanPay())
throw new VerificationException("New payment signature was not signed with the right SIGHASH flags."); 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. // Now check the signature is correct.
// Note that the client must sign with SIGHASH_{SINGLE/NONE} | SIGHASH_ANYONECANPAY to allow us to add additional // 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. // 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); throw new VerificationException("Signature does not verify on tx\n" + req.tx);
bestValueToMe = newValueToMe; bestValueToMe = newValueToMe;
bestValueSignature = signatureBytes; bestValueSignature = signatureBytes;
@ -338,103 +290,15 @@ public class PaymentChannelServerState {
return !fullyUsedUp; 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<Transaction> closedFuture = SettableFuture.create();
/** /**
* <p>Closes this channel and broadcasts the highest value payment transaction on the network.</p> * <p>Closes this channel and broadcasts the highest value payment transaction on the network.</p>
* *
* <p>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}.</p>
*
* <p>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.
* </p>
*
* @return a future which completes when the provided multisig contract successfully broadcasts, or throws if the * @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 * broadcast fails for some reason. Note that if the network simply rejects the transaction, this future
* will never complete, a timeout should be used. * 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. * @throws InsufficientMoneyException If the payment tx would have cost more in fees to spend than it is worth.
*/ */
public synchronized ListenableFuture<Transaction> close() throws InsufficientMoneyException { public abstract ListenableFuture<Transaction> 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<Transaction> future = broadcaster.broadcastTransaction(tx).future();
Futures.addCallback(future, new FutureCallback<Transaction>() {
@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;
}
/** /**
* Gets the highest payment to ourselves (which we will receive on settle(), not including fees) * 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) * Gets the fee paid in the final payment transaction (only available if settle() did not throw an exception)
*/ */
public synchronized Coin getFeePaid() { public abstract Coin getFeePaid();
checkState(state == State.CLOSED || state == State.CLOSING);
return feePaidForPayment;
}
/** /**
* Gets the multisig contract which was used to initialize this channel * Gets the multisig contract which was used to initialize this channel
*/ */
public synchronized Transaction getMultisigContract() { public synchronized Transaction getContract() {
checkState(multisigContract != null); checkState(contract != null);
return multisigContract; return contract;
} }
/** public long getExpiryTime() {
* Gets the client's refund transaction which they can spend to get the entire channel value back if it reaches its return minExpireTime;
* lock time.
*/
public synchronized long getRefundTransactionUnlockTime() {
checkState(state.compareTo(State.WAITING_FOR_MULTISIG_CONTRACT) > 0 && state != State.ERROR);
return refundTransactionUnlockTimeSecs;
} }
private synchronized void updateChannelInWallet() { protected synchronized void updateChannelInWallet() {
if (storedServerChannel != null) { if (storedServerChannel != null) {
storedServerChannel.updateValueToMe(bestValueToMe, bestValueSignature); storedServerChannel.updateValueToMe(bestValueToMe, bestValueSignature);
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) 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 * 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 * 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). * has fully opened (ie state == State.READY).
* *
* @param connectedHandler Optional {@link PaymentChannelServer} object that manages this object. This will * @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. * handler which can then do a TCP disconnect.
*/ */
public synchronized void storeChannelInWallet(@Nullable PaymentChannelServer connectedHandler) { public synchronized void storeChannelInWallet(@Nullable PaymentChannelServer connectedHandler) {
checkState(state == State.READY); stateMachine.checkState(State.READY);
if (storedServerChannel != null) if (storedServerChannel != null)
return; return;
log.info("Storing state with contract hash {}.", multisigContract.getHash()); log.info("Storing state with contract hash {}.", getContract().getHash());
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates) StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)
wallet.addOrGetExistingExtension(new StoredPaymentChannelServerStates(wallet, broadcaster)); 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) if (connectedHandler != null)
checkState(storedServerChannel.setConnectedHandler(connectedHandler, false) == connectedHandler); checkState(storedServerChannel.setConnectedHandler(connectedHandler, false) == connectedHandler);
channels.putChannel(storedServerChannel); 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();
} }

View File

@ -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<State, State> getStateTransitions() {
Multimap<State, State> 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<ECKey> 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;
}
/**
* <p>When the servers signature for the refund transaction is received, call this to verify it and sign the
* complete refund ourselves.</p>
*
* <p>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.</p>
*/
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;
}
}

View File

@ -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<State, State> getStateTransitions() {
Multimap<State, State> 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.<ECKey>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<Transaction> closedFuture = SettableFuture.create();
/**
* <p>Closes this channel and broadcasts the highest value payment transaction on the network.</p>
*
* <p>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}.</p>
*
* <p>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.
* </p>
*
* @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<Transaction> 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<Transaction> future = broadcaster.broadcastTransaction(tx).future();
Futures.addCallback(future, new FutureCallback<Transaction>() {
@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;
}
}

View File

@ -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<State, State> getStateTransitions() {
Multimap<State, State> 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;
}
}

View File

@ -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<State, State> getStateTransitions() {
Multimap<State, State> 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<Transaction> closedFuture = SettableFuture.create();
@Override
public synchronized ListenableFuture<Transaction> 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<Transaction> future = broadcaster.broadcastTransaction(tx).future();
Futures.addCallback(future, new FutureCallback<Transaction>() {
@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;
}
}

View File

@ -745,11 +745,11 @@ public final class ServerState {
com.google.protobuf.ByteString getContractTransaction(); com.google.protobuf.ByteString getContractTransaction();
/** /**
* <code>required bytes clientOutput = 5;</code> * <code>optional bytes clientOutput = 5;</code>
*/ */
boolean hasClientOutput(); boolean hasClientOutput();
/** /**
* <code>required bytes clientOutput = 5;</code> * <code>optional bytes clientOutput = 5;</code>
*/ */
com.google.protobuf.ByteString getClientOutput(); com.google.protobuf.ByteString getClientOutput();
@ -761,6 +761,32 @@ public final class ServerState {
* <code>required bytes myKey = 6;</code> * <code>required bytes myKey = 6;</code>
*/ */
com.google.protobuf.ByteString getMyKey(); com.google.protobuf.ByteString getMyKey();
/**
* <code>optional uint32 majorVersion = 7 [default = 1];</code>
*/
boolean hasMajorVersion();
/**
* <code>optional uint32 majorVersion = 7 [default = 1];</code>
*/
int getMajorVersion();
/**
* <code>optional bytes clientKey = 8;</code>
*
* <pre>
* Protocol version 2 only - the P2SH hash doesn't contain the required key
* </pre>
*/
boolean hasClientKey();
/**
* <code>optional bytes clientKey = 8;</code>
*
* <pre>
* Protocol version 2 only - the P2SH hash doesn't contain the required key
* </pre>
*/
com.google.protobuf.ByteString getClientKey();
} }
/** /**
* Protobuf type {@code paymentchannels.StoredServerPaymentChannel} * Protobuf type {@code paymentchannels.StoredServerPaymentChannel}
@ -848,6 +874,16 @@ public final class ServerState {
myKey_ = input.readBytes(); myKey_ = input.readBytes();
break; break;
} }
case 56: {
bitField0_ |= 0x00000040;
majorVersion_ = input.readUInt32();
break;
}
case 66: {
bitField0_ |= 0x00000080;
clientKey_ = input.readBytes();
break;
}
} }
} }
} catch (com.google.protobuf.InvalidProtocolBufferException e) { } catch (com.google.protobuf.InvalidProtocolBufferException e) {
@ -951,13 +987,13 @@ public final class ServerState {
public static final int CLIENTOUTPUT_FIELD_NUMBER = 5; public static final int CLIENTOUTPUT_FIELD_NUMBER = 5;
private com.google.protobuf.ByteString clientOutput_; private com.google.protobuf.ByteString clientOutput_;
/** /**
* <code>required bytes clientOutput = 5;</code> * <code>optional bytes clientOutput = 5;</code>
*/ */
public boolean hasClientOutput() { public boolean hasClientOutput() {
return ((bitField0_ & 0x00000010) == 0x00000010); return ((bitField0_ & 0x00000010) == 0x00000010);
} }
/** /**
* <code>required bytes clientOutput = 5;</code> * <code>optional bytes clientOutput = 5;</code>
*/ */
public com.google.protobuf.ByteString getClientOutput() { public com.google.protobuf.ByteString getClientOutput() {
return clientOutput_; return clientOutput_;
@ -978,6 +1014,44 @@ public final class ServerState {
return myKey_; return myKey_;
} }
public static final int MAJORVERSION_FIELD_NUMBER = 7;
private int majorVersion_;
/**
* <code>optional uint32 majorVersion = 7 [default = 1];</code>
*/
public boolean hasMajorVersion() {
return ((bitField0_ & 0x00000040) == 0x00000040);
}
/**
* <code>optional uint32 majorVersion = 7 [default = 1];</code>
*/
public int getMajorVersion() {
return majorVersion_;
}
public static final int CLIENTKEY_FIELD_NUMBER = 8;
private com.google.protobuf.ByteString clientKey_;
/**
* <code>optional bytes clientKey = 8;</code>
*
* <pre>
* Protocol version 2 only - the P2SH hash doesn't contain the required key
* </pre>
*/
public boolean hasClientKey() {
return ((bitField0_ & 0x00000080) == 0x00000080);
}
/**
* <code>optional bytes clientKey = 8;</code>
*
* <pre>
* Protocol version 2 only - the P2SH hash doesn't contain the required key
* </pre>
*/
public com.google.protobuf.ByteString getClientKey() {
return clientKey_;
}
private void initFields() { private void initFields() {
bestValueToMe_ = 0L; bestValueToMe_ = 0L;
bestValueSignature_ = com.google.protobuf.ByteString.EMPTY; bestValueSignature_ = com.google.protobuf.ByteString.EMPTY;
@ -985,6 +1059,8 @@ public final class ServerState {
contractTransaction_ = com.google.protobuf.ByteString.EMPTY; contractTransaction_ = com.google.protobuf.ByteString.EMPTY;
clientOutput_ = com.google.protobuf.ByteString.EMPTY; clientOutput_ = com.google.protobuf.ByteString.EMPTY;
myKey_ = com.google.protobuf.ByteString.EMPTY; myKey_ = com.google.protobuf.ByteString.EMPTY;
majorVersion_ = 1;
clientKey_ = com.google.protobuf.ByteString.EMPTY;
} }
private byte memoizedIsInitialized = -1; private byte memoizedIsInitialized = -1;
public final boolean isInitialized() { public final boolean isInitialized() {
@ -1004,10 +1080,6 @@ public final class ServerState {
memoizedIsInitialized = 0; memoizedIsInitialized = 0;
return false; return false;
} }
if (!hasClientOutput()) {
memoizedIsInitialized = 0;
return false;
}
if (!hasMyKey()) { if (!hasMyKey()) {
memoizedIsInitialized = 0; memoizedIsInitialized = 0;
return false; return false;
@ -1037,6 +1109,12 @@ public final class ServerState {
if (((bitField0_ & 0x00000020) == 0x00000020)) { if (((bitField0_ & 0x00000020) == 0x00000020)) {
output.writeBytes(6, myKey_); output.writeBytes(6, myKey_);
} }
if (((bitField0_ & 0x00000040) == 0x00000040)) {
output.writeUInt32(7, majorVersion_);
}
if (((bitField0_ & 0x00000080) == 0x00000080)) {
output.writeBytes(8, clientKey_);
}
getUnknownFields().writeTo(output); getUnknownFields().writeTo(output);
} }
@ -1070,6 +1148,14 @@ public final class ServerState {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeBytesSize(6, myKey_); .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(); size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size; memoizedSerializedSize = size;
return size; return size;
@ -1203,6 +1289,10 @@ public final class ServerState {
bitField0_ = (bitField0_ & ~0x00000010); bitField0_ = (bitField0_ & ~0x00000010);
myKey_ = com.google.protobuf.ByteString.EMPTY; myKey_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000020); bitField0_ = (bitField0_ & ~0x00000020);
majorVersion_ = 1;
bitField0_ = (bitField0_ & ~0x00000040);
clientKey_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000080);
return this; return this;
} }
@ -1255,6 +1345,14 @@ public final class ServerState {
to_bitField0_ |= 0x00000020; to_bitField0_ |= 0x00000020;
} }
result.myKey_ = myKey_; 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_; result.bitField0_ = to_bitField0_;
onBuilt(); onBuilt();
return result; return result;
@ -1289,6 +1387,12 @@ public final class ServerState {
if (other.hasMyKey()) { if (other.hasMyKey()) {
setMyKey(other.getMyKey()); setMyKey(other.getMyKey());
} }
if (other.hasMajorVersion()) {
setMajorVersion(other.getMajorVersion());
}
if (other.hasClientKey()) {
setClientKey(other.getClientKey());
}
this.mergeUnknownFields(other.getUnknownFields()); this.mergeUnknownFields(other.getUnknownFields());
return this; return this;
} }
@ -1306,10 +1410,6 @@ public final class ServerState {
return false; return false;
} }
if (!hasClientOutput()) {
return false;
}
if (!hasMyKey()) { if (!hasMyKey()) {
return false; return false;
@ -1472,19 +1572,19 @@ public final class ServerState {
private com.google.protobuf.ByteString clientOutput_ = com.google.protobuf.ByteString.EMPTY; private com.google.protobuf.ByteString clientOutput_ = com.google.protobuf.ByteString.EMPTY;
/** /**
* <code>required bytes clientOutput = 5;</code> * <code>optional bytes clientOutput = 5;</code>
*/ */
public boolean hasClientOutput() { public boolean hasClientOutput() {
return ((bitField0_ & 0x00000010) == 0x00000010); return ((bitField0_ & 0x00000010) == 0x00000010);
} }
/** /**
* <code>required bytes clientOutput = 5;</code> * <code>optional bytes clientOutput = 5;</code>
*/ */
public com.google.protobuf.ByteString getClientOutput() { public com.google.protobuf.ByteString getClientOutput() {
return clientOutput_; return clientOutput_;
} }
/** /**
* <code>required bytes clientOutput = 5;</code> * <code>optional bytes clientOutput = 5;</code>
*/ */
public Builder setClientOutput(com.google.protobuf.ByteString value) { public Builder setClientOutput(com.google.protobuf.ByteString value) {
if (value == null) { if (value == null) {
@ -1496,7 +1596,7 @@ public final class ServerState {
return this; return this;
} }
/** /**
* <code>required bytes clientOutput = 5;</code> * <code>optional bytes clientOutput = 5;</code>
*/ */
public Builder clearClientOutput() { public Builder clearClientOutput() {
bitField0_ = (bitField0_ & ~0x00000010); bitField0_ = (bitField0_ & ~0x00000010);
@ -1540,6 +1640,89 @@ public final class ServerState {
return this; return this;
} }
private int majorVersion_ = 1;
/**
* <code>optional uint32 majorVersion = 7 [default = 1];</code>
*/
public boolean hasMajorVersion() {
return ((bitField0_ & 0x00000040) == 0x00000040);
}
/**
* <code>optional uint32 majorVersion = 7 [default = 1];</code>
*/
public int getMajorVersion() {
return majorVersion_;
}
/**
* <code>optional uint32 majorVersion = 7 [default = 1];</code>
*/
public Builder setMajorVersion(int value) {
bitField0_ |= 0x00000040;
majorVersion_ = value;
onChanged();
return this;
}
/**
* <code>optional uint32 majorVersion = 7 [default = 1];</code>
*/
public Builder clearMajorVersion() {
bitField0_ = (bitField0_ & ~0x00000040);
majorVersion_ = 1;
onChanged();
return this;
}
private com.google.protobuf.ByteString clientKey_ = com.google.protobuf.ByteString.EMPTY;
/**
* <code>optional bytes clientKey = 8;</code>
*
* <pre>
* Protocol version 2 only - the P2SH hash doesn't contain the required key
* </pre>
*/
public boolean hasClientKey() {
return ((bitField0_ & 0x00000080) == 0x00000080);
}
/**
* <code>optional bytes clientKey = 8;</code>
*
* <pre>
* Protocol version 2 only - the P2SH hash doesn't contain the required key
* </pre>
*/
public com.google.protobuf.ByteString getClientKey() {
return clientKey_;
}
/**
* <code>optional bytes clientKey = 8;</code>
*
* <pre>
* Protocol version 2 only - the P2SH hash doesn't contain the required key
* </pre>
*/
public Builder setClientKey(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000080;
clientKey_ = value;
onChanged();
return this;
}
/**
* <code>optional bytes clientKey = 8;</code>
*
* <pre>
* Protocol version 2 only - the P2SH hash doesn't contain the required key
* </pre>
*/
public Builder clearClientKey() {
bitField0_ = (bitField0_ & ~0x00000080);
clientKey_ = getDefaultInstance().getClientKey();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:paymentchannels.StoredServerPaymentChannel) // @@protoc_insertion_point(builder_scope:paymentchannels.StoredServerPaymentChannel)
} }
@ -1573,13 +1756,14 @@ public final class ServerState {
"\n storedserverpaymentchannel.proto\022\017paym" + "\n storedserverpaymentchannel.proto\022\017paym" +
"entchannels\"\\\n\033StoredServerPaymentChanne" + "entchannels\"\\\n\033StoredServerPaymentChanne" +
"ls\022=\n\010channels\030\001 \003(\0132+.paymentchannels.S" + "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" + "erPaymentChannel\022\025\n\rbestValueToMe\030\001 \002(\004\022" +
"\032\n\022bestValueSignature\030\002 \001(\014\022\'\n\037refundTra" + "\032\n\022bestValueSignature\030\002 \001(\014\022\'\n\037refundTra" +
"nsactionUnlockTimeSecs\030\003 \002(\004\022\033\n\023contract" + "nsactionUnlockTimeSecs\030\003 \002(\004\022\033\n\023contract" +
"Transaction\030\004 \002(\014\022\024\n\014clientOutput\030\005 \002(\014\022" + "Transaction\030\004 \002(\014\022\024\n\014clientOutput\030\005 \001(\014\022" +
"\r\n\005myKey\030\006 \002(\014B.\n\037org.bitcoinj.protocols" + "\r\n\005myKey\030\006 \002(\014\022\027\n\014majorVersion\030\007 \001(\r:\0011\022" +
".channelsB\013ServerState" "\021\n\tclientKey\030\010 \001(\014B.\n\037org.bitcoinj.proto",
"cols.channelsB\013ServerState"
}; };
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() { new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() {
@ -1604,7 +1788,7 @@ public final class ServerState {
internal_static_paymentchannels_StoredServerPaymentChannel_fieldAccessorTable = new internal_static_paymentchannels_StoredServerPaymentChannel_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable( com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_paymentchannels_StoredServerPaymentChannel_descriptor, 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) // @@protoc_insertion_point(outer_class_scope)

View File

@ -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 <State> An enum of states to use
*/
public class StateMachine<State extends Enum<State>> {
private State currentState;
private final Multimap<State, State> transitions;
public StateMachine(State startState, Multimap<State, State> 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();
}
}

View File

@ -17,6 +17,8 @@
package org.bitcoinj.protocols.channels; package org.bitcoinj.protocols.channels;
import org.bitcoinj.core.*; import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.utils.Threading; import org.bitcoinj.utils.Threading;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashMultimap; import com.google.common.collect.HashMultimap;
@ -279,14 +281,18 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
(!params.hasMaxMoney() || channel.refundFees.compareTo(params.getMaxMoney()) <= 0)); (!params.hasMaxMoney() || channel.refundFees.compareTo(params.getMaxMoney()) <= 0));
checkNotNull(channel.myKey.getPubKey()); checkNotNull(channel.myKey.getPubKey());
checkState(channel.refund.getConfidence().getSource() == TransactionConfidence.Source.SELF); checkState(channel.refund.getConfidence().getSource() == TransactionConfidence.Source.SELF);
checkNotNull(channel.myKey.getPubKey());
final ClientState.StoredClientPaymentChannel.Builder value = ClientState.StoredClientPaymentChannel.newBuilder() final ClientState.StoredClientPaymentChannel.Builder value = ClientState.StoredClientPaymentChannel.newBuilder()
.setMajorVersion(channel.majorVersion)
.setId(ByteString.copyFrom(channel.id.getBytes())) .setId(ByteString.copyFrom(channel.id.getBytes()))
.setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize())) .setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize()))
.setRefundFees(channel.refundFees.value)
.setRefundTransaction(ByteString.copyFrom(channel.refund.bitcoinSerialize())) .setRefundTransaction(ByteString.copyFrom(channel.refund.bitcoinSerialize()))
.setMyKey(ByteString.copyFrom(new byte[0])) // Not used, but protobuf message requires .setMyKey(ByteString.copyFrom(new byte[0])) // Not used, but protobuf message requires
.setMyPublicKey(ByteString.copyFrom(channel.myKey.getPubKey())) .setMyPublicKey(ByteString.copyFrom(channel.myKey.getPubKey()))
.setServerKey(ByteString.copyFrom(channel.serverKey.getPubKey()))
.setValueToMe(channel.valueToMe.value) .setValueToMe(channel.valueToMe.value)
.setRefundFees(channel.refundFees.value); .setExpiryTime(channel.expiryTime);
if (channel.close != null) if (channel.close != null)
value.setCloseTransactionHash(ByteString.copyFrom(channel.close.getHash().getBytes())); value.setCloseTransactionHash(ByteString.copyFrom(channel.close.getHash().getBytes()));
builder.addChannels(value); builder.addChannels(value);
@ -311,12 +317,17 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
ECKey myKey = (storedState.getMyKey().isEmpty()) ? ECKey myKey = (storedState.getMyKey().isEmpty()) ?
containingWallet.findKeyFromPubKey(storedState.getMyPublicKey().toByteArray()) : containingWallet.findKeyFromPubKey(storedState.getMyPublicKey().toByteArray()) :
ECKey.fromPrivate(storedState.getMyKey().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()), params.getDefaultSerializer().makeTransaction(storedState.getContractTransaction().toByteArray()),
refundTransaction, refundTransaction,
myKey, myKey,
serverKey,
Coin.valueOf(storedState.getValueToMe()), Coin.valueOf(storedState.getValueToMe()),
Coin.valueOf(storedState.getRefundFees()), false); Coin.valueOf(storedState.getRefundFees()),
storedState.getExpiryTime(),
false);
if (storedState.hasCloseTransactionHash()) { if (storedState.hasCloseTransactionHash()) {
Sha256Hash closeTxHash = Sha256Hash.wrap(storedState.getCloseTransactionHash().toByteArray()); Sha256Hash closeTxHash = Sha256Hash.wrap(storedState.getCloseTransactionHash().toByteArray());
channel.close = containingWallet.getTransaction(closeTxHash); channel.close = containingWallet.getTransaction(closeTxHash);
@ -352,29 +363,43 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
* when they expire. * when they expire.
*/ */
class StoredClientChannel { class StoredClientChannel {
int majorVersion;
Sha256Hash id; Sha256Hash id;
Transaction contract, refund; Transaction contract, refund;
// The expiry time of the contract in protocol v2.
long expiryTime;
// The transaction that closed the channel (generated by the server) // The transaction that closed the channel (generated by the server)
Transaction close; Transaction close;
ECKey myKey; ECKey myKey;
ECKey serverKey;
Coin valueToMe, refundFees; Coin valueToMe, refundFees;
// In-memory flag to indicate intent to resume this channel (or that the channel is already in use) // In-memory flag to indicate intent to resume this channel (or that the channel is already in use)
boolean active = false; boolean active = false;
StoredClientChannel(Sha256Hash id, Transaction contract, Transaction refund, ECKey myKey, Coin valueToMe, StoredClientChannel(int majorVersion, Sha256Hash id, Transaction contract, Transaction refund, ECKey myKey, ECKey serverKey, Coin valueToMe,
Coin refundFees, boolean active) { Coin refundFees, long expiryTime, boolean active) {
this.majorVersion = majorVersion;
this.id = id; this.id = id;
this.contract = contract; this.contract = contract;
this.refund = refund; this.refund = refund;
this.myKey = myKey; this.myKey = myKey;
this.serverKey = serverKey;
this.valueToMe = valueToMe; this.valueToMe = valueToMe;
this.refundFees = refundFees; this.refundFees = refundFees;
this.expiryTime = expiryTime;
this.active = active; this.active = active;
} }
long expiryTimeSeconds() { 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 @Override
@ -382,13 +407,16 @@ class StoredClientChannel {
final String newline = String.format(Locale.US, "%n"); final String newline = String.format(Locale.US, "%n");
final String closeStr = close == null ? "still open" : close.toString().replaceAll(newline, newline + " "); 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" + return String.format(Locale.US, "Stored client channel for server ID %s (%s)%n" +
" Version: %d%n" +
" Key: %s%n" + " Key: %s%n" +
" Server key: %s%n" +
" Value left: %s%n" + " Value left: %s%n" +
" Refund fees: %s%n" + " Refund fees: %s%n" +
" Expiry : %s%n" +
" Contract: %s" + " Contract: %s" +
"Refund: %s" + "Refund: %s" +
"Close: %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 + " "), contract.toString().replaceAll(newline, newline + " "),
refund.toString().replaceAll(newline, newline + " "), refund.toString().replaceAll(newline, newline + " "),
closeStr); closeStr);

View File

@ -91,7 +91,7 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
/** /**
* <p>Closes the given channel using {@link ServerConnectionEventHandler#closeChannel()} and * <p>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.</p> * broadcast the latest payment transaction.</p>
* *
* <p>Removes the given channel from this set of {@link StoredServerChannel}s and notifies the wallet of a change to * <p>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); checkState(channel.refundTransactionUnlockTimeSecs > 0);
checkNotNull(channel.myKey.getPrivKeyBytes()); checkNotNull(channel.myKey.getPrivKeyBytes());
ServerState.StoredServerPaymentChannel.Builder channelBuilder = ServerState.StoredServerPaymentChannel.newBuilder() ServerState.StoredServerPaymentChannel.Builder channelBuilder = ServerState.StoredServerPaymentChannel.newBuilder()
.setMajorVersion(channel.majorVersion)
.setBestValueToMe(channel.bestValueToMe.value) .setBestValueToMe(channel.bestValueToMe.value)
.setRefundTransactionUnlockTimeSecs(channel.refundTransactionUnlockTimeSecs) .setRefundTransactionUnlockTimeSecs(channel.refundTransactionUnlockTimeSecs)
.setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize())) .setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize()))
.setClientOutput(ByteString.copyFrom(channel.clientOutput.bitcoinSerialize()))
.setMyKey(ByteString.copyFrom(channel.myKey.getPrivKeyBytes())); .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) if (channel.bestValueSignature != null)
channelBuilder.setBestValueSignature(ByteString.copyFrom(channel.bestValueSignature)); channelBuilder.setBestValueSignature(ByteString.copyFrom(channel.bestValueSignature));
builder.addChannels(channelBuilder); builder.addChannels(channelBuilder);
@ -246,11 +251,21 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
ServerState.StoredServerPaymentChannels states = ServerState.StoredServerPaymentChannels.parseFrom(data); ServerState.StoredServerPaymentChannels states = ServerState.StoredServerPaymentChannels.parseFrom(data);
NetworkParameters params = containingWallet.getParams(); NetworkParameters params = containingWallet.getParams();
for (ServerState.StoredServerPaymentChannel storedState : states.getChannelsList()) { 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, StoredServerChannel channel = new StoredServerChannel(null,
majorVersion,
params.getDefaultSerializer().makeTransaction(storedState.getContractTransaction().toByteArray()), params.getDefaultSerializer().makeTransaction(storedState.getContractTransaction().toByteArray()),
new TransactionOutput(params, null, storedState.getClientOutput().toByteArray(), 0), clientOutput,
storedState.getRefundTransactionUnlockTimeSecs(), storedState.getRefundTransactionUnlockTimeSecs(),
ECKey.fromPrivate(storedState.getMyKey().toByteArray()), ECKey.fromPrivate(storedState.getMyKey().toByteArray()),
clientKey,
Coin.valueOf(storedState.getBestValueToMe()), Coin.valueOf(storedState.getBestValueToMe()),
storedState.hasBestValueSignature() ? storedState.getBestValueSignature().toByteArray() : null); storedState.hasBestValueSignature() ? storedState.getBestValueSignature().toByteArray() : null);
putChannel(channel); putChannel(channel);

View File

@ -30,24 +30,32 @@ import static com.google.common.base.Preconditions.checkArgument;
* time approaches. * time approaches.
*/ */
public class StoredServerChannel { public class StoredServerChannel {
/**
* Channel version number. Currently can only be version 1
*/
int majorVersion;
Coin bestValueToMe; Coin bestValueToMe;
byte[] bestValueSignature; byte[] bestValueSignature;
long refundTransactionUnlockTimeSecs; long refundTransactionUnlockTimeSecs;
Transaction contract; Transaction contract;
TransactionOutput clientOutput; TransactionOutput clientOutput;
ECKey myKey; 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. // 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. // Used as a flag to prevent duplicate connections and to disconnect the channel if its expire time approaches.
private PaymentChannelServer connectedHandler = null; private PaymentChannelServer connectedHandler = null;
PaymentChannelServerState state = null; PaymentChannelServerState state = null;
StoredServerChannel(@Nullable PaymentChannelServerState state, Transaction contract, TransactionOutput clientOutput, StoredServerChannel(@Nullable PaymentChannelServerState state, int majorVersion, Transaction contract, TransactionOutput clientOutput,
long refundTransactionUnlockTimeSecs, ECKey myKey, Coin bestValueToMe, @Nullable byte[] bestValueSignature) { long refundTransactionUnlockTimeSecs, ECKey myKey, ECKey clientKey, Coin bestValueToMe, @Nullable byte[] bestValueSignature) {
this.majorVersion = majorVersion;
this.contract = contract; this.contract = contract;
this.clientOutput = clientOutput; this.clientOutput = clientOutput;
this.refundTransactionUnlockTimeSecs = refundTransactionUnlockTimeSecs; this.refundTransactionUnlockTimeSecs = refundTransactionUnlockTimeSecs;
this.myKey = myKey; this.myKey = myKey;
this.clientKey = clientKey;
this.bestValueToMe = bestValueToMe; this.bestValueToMe = bestValueToMe;
this.bestValueSignature = bestValueSignature; this.bestValueSignature = bestValueSignature;
this.state = state; this.state = state;
@ -96,8 +104,18 @@ public class StoredServerChannel {
* @param broadcaster The {@link TransactionBroadcaster} which will be used to broadcast contract/payment transactions. * @param broadcaster The {@link TransactionBroadcaster} which will be used to broadcast contract/payment transactions.
*/ */
public synchronized PaymentChannelServerState getOrCreateState(Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException { public synchronized PaymentChannelServerState getOrCreateState(Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException {
if (state == null) if (state == null) {
state = new PaymentChannelServerState(this, wallet, broadcaster); 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); checkArgument(wallet == state.wallet);
return state; return state;
} }
@ -106,12 +124,13 @@ public class StoredServerChannel {
public synchronized String toString() { public synchronized String toString() {
final String newline = String.format(Locale.US, "%n"); final String newline = String.format(Locale.US, "%n");
return String.format(Locale.US, "Stored server channel (%s)%n" + return String.format(Locale.US, "Stored server channel (%s)%n" +
" Version: %d%n" +
" Key: %s%n" + " Key: %s%n" +
" Value to me: %s%n" + " Value to me: %s%n" +
" Client output: %s%n" + " Client output: %s%n" +
" Refund unlock: %s (%d unix time)%n" + " Refund unlock: %s (%d unix time)%n" +
" Contract: %s%n", " Contract: %s%n",
connectedHandler != null ? "connected" : "disconnected", myKey, bestValueToMe, connectedHandler != null ? "connected" : "disconnected", majorVersion, myKey, bestValueToMe,
clientOutput, new Date(refundTransactionUnlockTimeSecs * 1000), refundTransactionUnlockTimeSecs, clientOutput, new Date(refundTransactionUnlockTimeSecs * 1000), refundTransactionUnlockTimeSecs,
contract.toString().replaceAll(newline, newline + " ")); contract.toString().replaceAll(newline, newline + " "));
} }

View File

@ -457,10 +457,31 @@ public class ScriptBuilder {
return builder.build(); return builder.build();
} }
public static Script createCLTVPaymentChannelInput(TransactionSignature from, TransactionSignature to) { public static Script createCLTVPaymentChannelP2SHRefund(TransactionSignature signature, Script redeemScript) {
ScriptBuilder builder = new ScriptBuilder(); ScriptBuilder builder = new ScriptBuilder();
builder.data(from.encodeToBitcoin()); builder.data(signature.encodeToBitcoin());
builder.data(to.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 builder.smallNum(1); // Use the CHECKLOCKTIMEVERIFY if branch
return builder.build(); return builder.build();
} }

View File

@ -178,11 +178,17 @@ message ReturnRefund {
// Sent from the primary to the secondary to complete initialization. // Sent from the primary to the secondary to complete initialization.
message ProvideContract { message ProvideContract {
// The serialized bytes of the transaction in Satoshi format. // 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 // * 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 // 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 // * 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 // 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") // 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; required bytes tx = 1;
// To open the channel, an initial payment of the server-specified dust limit value must be // 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 // no payment tx having been provided at all, or a payment that is smaller than the dust
// limit being provided. // limit being provided.
required UpdatePayment initial_payment = 2; 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 // This message can only be used by the primary after it has received a CHANNEL_OPEN message. It

View File

@ -43,9 +43,15 @@ message StoredClientPaymentChannel {
// Deprecated, key is already stored in the wallet, and found using myPublicKey; // Deprecated, key is already stored in the wallet, and found using myPublicKey;
required bytes myKey = 4; required bytes myKey = 4;
required uint64 valueToMe = 5; required uint64 valueToMe = 5;
// Fees required to refund the transaction.
required uint64 refundFees = 6; required uint64 refundFees = 6;
// When set, the hash of the transaction that was presented by the server for closure of the channel. // 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 spends the contractTransaction and is expected to be broadcast to the network by the server.
// It's supposed to be in the wallet already. // It's supposed to be in the wallet already.
optional bytes closeTransactionHash = 7; 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;
} }

View File

@ -39,6 +39,9 @@ message StoredServerPaymentChannel {
optional bytes bestValueSignature = 2; optional bytes bestValueSignature = 2;
required uint64 refundTransactionUnlockTimeSecs = 3; required uint64 refundTransactionUnlockTimeSecs = 3;
required bytes contractTransaction = 4; required bytes contractTransaction = 4;
required bytes clientOutput = 5; optional bytes clientOutput = 5;
required bytes myKey = 6; 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;
} }

View File

@ -29,16 +29,19 @@ import org.bitcoin.paymentchannel.Protos;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.lang.model.type.ExecutableType;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; 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.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@RunWith(Parameterized.class)
public class ChannelConnectionTest extends TestWithWallet { public class ChannelConnectionTest extends TestWithWallet {
private static final int CLIENT_MAJOR_VERSION = 1; private static final int CLIENT_MAJOR_VERSION = 1;
private Wallet serverWallet; 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<PaymentChannelClient.VersionSelector> data() {
return Arrays.asList(
PaymentChannelClient.VersionSelector.VERSION_1,
PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1);
}
@Parameterized.Parameter
public PaymentChannelClient.VersionSelector versionSelector;
/**
* Returns <code>true</code> if we are using a protocol version that requires the exchange of refunds.
*/
private boolean useRefunds() {
return versionSelector == PaymentChannelClient.VersionSelector.VERSION_1;
}
/**
* Returns <code>true</code> if the contract being used is a multisig contract
* @return
*/
private boolean isMultiSigContract() {
return versionSelector == PaymentChannelClient.VersionSelector.VERSION_1;
}
@Override @Override
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@ -132,7 +165,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void exectuteSimpleChannelTest(KeyParameter userKeySetup) throws Exception { 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. // Test with network code and without any issues. We'll broadcast two txns: multisig contract and settle transaction.
final SettableFuture<ListenableFuture<PaymentChannelServerState>> serverCloseFuture = SettableFuture.create(); final SettableFuture<ListenableFuture<PaymentChannelV1ServerState>> serverCloseFuture = SettableFuture.create();
final SettableFuture<Sha256Hash> channelOpenFuture = SettableFuture.create(); final SettableFuture<Sha256Hash> channelOpenFuture = SettableFuture.create();
final BlockingQueue<ChannelTestUtils.UpdatePair> q = new LinkedBlockingQueue<ChannelTestUtils.UpdatePair>(); final BlockingQueue<ChannelTestUtils.UpdatePair> q = new LinkedBlockingQueue<ChannelTestUtils.UpdatePair>();
final PaymentChannelServerListener server = new PaymentChannelServerListener(mockBroadcaster, serverWallet, 30, COIN, final PaymentChannelServerListener server = new PaymentChannelServerListener(mockBroadcaster, serverWallet, 30, COIN,
@ -162,7 +195,7 @@ public class ChannelConnectionTest extends TestWithWallet {
server.bindAndStart(4243); server.bindAndStart(4243);
PaymentChannelClientConnection client = new PaymentChannelClientConnection( 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. // Wait for the multi-sig tx to be transmitted.
broadcastTxPause.release(); broadcastTxPause.release();
@ -213,6 +246,10 @@ public class ChannelConnectionTest extends TestWithWallet {
broadcastTxPause.release(); broadcastTxPause.release();
Transaction settleTx = broadcasts.take(); 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()); assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState());
if (!serverState.getBestValueToMe().equals(amount) || !serverState.getFeePaid().equals(Coin.ZERO)) if (!serverState.getBestValueToMe().equals(amount) || !serverState.getFeePaid().equals(Coin.ZERO))
fail(); fail();
@ -220,7 +257,7 @@ public class ChannelConnectionTest extends TestWithWallet {
// Send the settle TX to the client wallet. // Send the settle TX to the client wallet.
sendMoneyToWallet(settleTx, AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(settleTx, AbstractBlockChain.NewBlockType.BEST_CHAIN);
assertEquals(PaymentChannelClientState.State.CLOSED, client.state().getState()); assertTrue(client.state().getState() == PaymentChannelClientState.State.CLOSED);
server.close(); server.close();
server.close(); server.close();
@ -235,9 +272,13 @@ public class ChannelConnectionTest extends TestWithWallet {
@Test @Test
public void testServerErrorHandling_badTransaction() throws Exception { 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. // Gives the server crap and checks proper error responses are sent.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); 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; PaymentChannelServer server = pair.server;
server.connectionOpen(); server.connectionOpen();
client.connectionOpen(); client.connectionOpen();
@ -262,7 +303,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testServerErrorHandling_killSocketOnClose() throws Exception { public void testServerErrorHandling_killSocketOnClose() throws Exception {
// Make sure the server closes the socket on CLOSE // Make sure the server closes the socket on CLOSE
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); 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; PaymentChannelServer server = pair.server;
server.connectionOpen(); server.connectionOpen();
client.connectionOpen(); client.connectionOpen();
@ -280,7 +321,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testServerErrorHandling_killSocketOnError() throws Exception { public void testServerErrorHandling_killSocketOnError() throws Exception {
// Make sure the server closes the socket on ERROR // Make sure the server closes the socket on ERROR
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); 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; PaymentChannelServer server = pair.server;
server.connectionOpen(); server.connectionOpen();
client.connectionOpen(); client.connectionOpen();
@ -304,7 +345,7 @@ public class ChannelConnectionTest extends TestWithWallet {
// Open up a normal channel. // Open up a normal channel.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen(); 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; PaymentChannelServer server = pair.server;
client.connectionOpen(); client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); 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); final Protos.TwoWayChannelMessage initiateMsg = pair.serverRecorder.checkNextMsg(MessageType.INITIATE);
Coin minPayment = Coin.valueOf(initiateMsg.getInitiate().getMinPayment()); Coin minPayment = Coin.valueOf(initiateMsg.getInitiate().getMinPayment());
client.receiveMessage(initiateMsg); client.receiveMessage(initiateMsg);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); if (useRefunds()) {
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
}
broadcastTxPause.release(); broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
broadcasts.take(); broadcasts.take();
@ -357,7 +400,7 @@ public class ChannelConnectionTest extends TestWithWallet {
// Open up a normal channel. // Open up a normal channel.
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen(); 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; PaymentChannelServer server = pair.server;
client.connectionOpen(); client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); 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); final Protos.TwoWayChannelMessage initiateMsg = pair.serverRecorder.checkNextMsg(MessageType.INITIATE);
Coin minPayment = Coin.valueOf(initiateMsg.getInitiate().getMinPayment()); Coin minPayment = Coin.valueOf(initiateMsg.getInitiate().getMinPayment());
client.receiveMessage(initiateMsg); client.receiveMessage(initiateMsg);
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); if (useRefunds()) {
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
}
broadcastTxPause.release(); broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
broadcasts.take(); broadcasts.take();
@ -415,7 +460,7 @@ public class ChannelConnectionTest extends TestWithWallet {
(StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); (StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); 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; server = pair.server;
client.connectionOpen(); client.connectionOpen();
server.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. // Now open up a new client with the same id and make sure the server disconnects the previous client.
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); 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; server = pair.server;
client.connectionOpen(); client.connectionOpen();
server.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. // Make sure the server allows two simultaneous opens. It will close the first and allow resumption of the second.
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); 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; server = pair.server;
client.connectionOpen(); client.connectionOpen();
server.connectionOpen(); server.connectionOpen();
@ -484,7 +529,11 @@ public class ChannelConnectionTest extends TestWithWallet {
StoredPaymentChannelClientStates newClientStates = new StoredPaymentChannelClientStates(wallet, mockBroadcaster); StoredPaymentChannelClientStates newClientStates = new StoredPaymentChannelClientStates(wallet, mockBroadcaster);
newClientStates.deserializeWalletExtension(wallet, clientStoredChannels.serializeWalletExtension()); newClientStates.deserializeWalletExtension(wallet, clientStoredChannels.serializeWalletExtension());
broadcastTxPause.release(); 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(); broadcastTxPause.release();
assertEquals(TransactionConfidence.Source.SELF, broadcasts.take().getConfidence().getSource()); assertEquals(TransactionConfidence.Source.SELF, broadcasts.take().getConfidence().getSource());
assertTrue(broadcasts.isEmpty()); assertTrue(broadcasts.isEmpty());
@ -534,7 +583,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testClientUnknownVersion() throws Exception { public void testClientUnknownVersion() throws Exception {
// Tests client rejects unknown version // Tests client rejects unknown version
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); 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(); client.connectionOpen();
pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION); pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder() client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
@ -554,7 +603,7 @@ public class ChannelConnectionTest extends TestWithWallet {
// Tests that clients reject too large time windows // Tests that clients reject too large time windows
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster, 100); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster, 100);
PaymentChannelServer server = pair.server; 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(); client.connectionOpen();
server.connectionOpen(); server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -579,7 +628,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testValuesAreRespected() throws Exception { public void testValuesAreRespected() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server; 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(); client.connectionOpen();
server.connectionOpen(); server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -605,7 +654,7 @@ public class ChannelConnectionTest extends TestWithWallet {
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
server = pair.server; server = pair.server;
final Coin myValue = COIN.multiply(10); 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(); client.connectionOpen();
server.connectionOpen(); server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -616,9 +665,15 @@ public class ChannelConnectionTest extends TestWithWallet {
.setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey())) .setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey()))
.setMinPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value)) .setMinPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value))
.setType(MessageType.INITIATE).build()); .setType(MessageType.INITIATE).build());
final Protos.TwoWayChannelMessage provideRefund = pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND); if (useRefunds()) {
Transaction refund = new Transaction(params, provideRefund.getProvideRefund().getTx().toByteArray()); final Protos.TwoWayChannelMessage provideRefund = pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND);
assertEquals(myValue, refund.getOutput(0).getValue()); 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 @Test
@ -627,7 +682,7 @@ public class ChannelConnectionTest extends TestWithWallet {
emptyWallet.freshReceiveKey(); emptyWallet.freshReceiveKey();
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server; 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(); client.connectionOpen();
server.connectionOpen(); server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -649,7 +704,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testClientRefusesNonCanonicalKey() throws Exception { public void testClientRefusesNonCanonicalKey() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server; 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(); client.connectionOpen();
server.connectionOpen(); server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -667,7 +722,7 @@ public class ChannelConnectionTest extends TestWithWallet {
public void testClientResumeNothing() throws Exception { public void testClientResumeNothing() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
PaymentChannelServer server = pair.server; 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(); client.connectionOpen();
server.connectionOpen(); server.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
@ -681,7 +736,7 @@ public class ChannelConnectionTest extends TestWithWallet {
@Test @Test
public void testClientRandomMessage() throws Exception { public void testClientRandomMessage() throws Exception {
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); 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(); client.connectionOpen();
pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION); pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
@ -702,14 +757,16 @@ public class ChannelConnectionTest extends TestWithWallet {
Sha256Hash someServerId = Sha256Hash.ZERO_HASH; Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen(); 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; PaymentChannelServer server = pair.server;
client.connectionOpen(); client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); if (useRefunds()) {
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
}
broadcastTxPause.release(); broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
broadcasts.take(); broadcasts.take();
@ -733,7 +790,7 @@ public class ChannelConnectionTest extends TestWithWallet {
client.connectionClosed(); client.connectionClosed();
// Now try opening a new channel with the same server ID and verify the client asks for a new channel. // 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(); client.connectionOpen();
Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION); Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
assertFalse(msg.getClientVersion().hasPreviousChannelContractHash()); assertFalse(msg.getClientVersion().hasPreviousChannelContractHash());
@ -748,14 +805,16 @@ public class ChannelConnectionTest extends TestWithWallet {
Sha256Hash someServerId = Sha256Hash.ZERO_HASH; Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen(); 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; PaymentChannelServer server = pair.server;
client.connectionOpen(); client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); if (useRefunds()) {
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
}
broadcastTxPause.release(); broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
broadcasts.take(); broadcasts.take();
@ -800,7 +859,7 @@ public class ChannelConnectionTest extends TestWithWallet {
Sha256Hash someServerId = Sha256Hash.ZERO_HASH; Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen(); 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; PaymentChannelServer server = pair.server;
client.connectionOpen(); client.connectionOpen();
final Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION); final Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
@ -808,8 +867,10 @@ public class ChannelConnectionTest extends TestWithWallet {
server.receiveMessage(msg); server.receiveMessage(msg);
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE)); client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND)); if (useRefunds()) {
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
}
broadcastTxPause.release(); broadcastTxPause.release();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
broadcasts.take(); broadcasts.take();
@ -829,7 +890,7 @@ public class ChannelConnectionTest extends TestWithWallet {
Sha256Hash someServerId = Sha256Hash.ZERO_HASH; Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster); ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
pair.server.connectionOpen(); 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; PaymentChannelServer server = pair.server;
client.connectionOpen(); client.connectionOpen();
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION)); server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));

View File

@ -6,8 +6,12 @@ import org.easymock.Capture;
import org.easymock.EasyMock; import org.easymock.EasyMock;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.KeyParameter;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage; 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.easymock.EasyMock.replay;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public class PaymentChannelClientTest { public class PaymentChannelClientTest {
private static final int CLIENT_MAJOR_VERSION = 1;
private Wallet wallet; private Wallet wallet;
private ECKey ecKey; private ECKey ecKey;
private Sha256Hash serverHash; private Sha256Hash serverHash;
@ -28,6 +32,22 @@ public class PaymentChannelClientTest {
public Capture<TwoWayChannelMessage> clientVersionCapture; public Capture<TwoWayChannelMessage> clientVersionCapture;
public int defaultTimeWindow = 86340; 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<PaymentChannelClient.VersionSelector> 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 @Before
public void before() { public void before() {
wallet = createMock(Wallet.class); wallet = createMock(Wallet.class);
@ -40,7 +60,7 @@ public class PaymentChannelClientTest {
@Test @Test
public void shouldSendClientVersionOnChannelOpen() throws Exception { 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)); connection.sendToServer(capture(clientVersionCapture));
EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap<String, WalletExtension>()); EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap<String, WalletExtension>());
replay(connection, wallet); replay(connection, wallet);
@ -52,7 +72,7 @@ public class PaymentChannelClientTest {
long timeWindow = 4000; long timeWindow = 4000;
KeyParameter userKey = null; KeyParameter userKey = null;
PaymentChannelClient dut = 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)); connection.sendToServer(capture(clientVersionCapture));
EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap<String, WalletExtension>()); EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap<String, WalletExtension>());
replay(connection, wallet); replay(connection, wallet);
@ -66,7 +86,8 @@ public class PaymentChannelClientTest {
assertEquals("Wrong type " + type, CLIENT_VERSION, type); assertEquals("Wrong type " + type, CLIENT_VERSION, type);
final Protos.ClientVersion clientVersion = response.getClientVersion(); final Protos.ClientVersion clientVersion = response.getClientVersion();
final int major = clientVersion.getMajor(); 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(); final long actualTimeWindow = clientVersion.getTimeWindowSecs();
assertEquals("Wrong timeWindow " + actualTimeWindow, expectedTimeWindow, actualTimeWindow ); assertEquals("Wrong timeWindow " + actualTimeWindow, expectedTimeWindow, actualTimeWindow );
} }

View File

@ -8,6 +8,11 @@ import org.bitcoin.paymentchannel.Protos;
import org.easymock.Capture; import org.easymock.Capture;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; 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 junit.framework.TestCase.assertTrue;
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage; 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.easymock.EasyMock.*;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public class PaymentChannelServerTest { public class PaymentChannelServerTest {
private static final int CLIENT_MAJOR_VERSION = 1;
private static final long SERVER_MAJOR_VERSION = 1;
public Wallet wallet; public Wallet wallet;
public PaymentChannelServer.ServerConnection connection; public PaymentChannelServer.ServerConnection connection;
public PaymentChannelServer dut; public PaymentChannelServer dut;
@ -35,6 +38,17 @@ public class PaymentChannelServerTest {
Utils.setMockClock(); 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<Integer> data() {
return Arrays.asList(1, 2);
}
@Parameterized.Parameter
public int protocolVersion;
@Test @Test
public void shouldAcceptDefaultTimeWindow() { public void shouldAcceptDefaultTimeWindow() {
@ -123,7 +137,7 @@ public class PaymentChannelServerTest {
final MessageType type = response.getType(); final MessageType type = response.getType();
assertEquals("Wrong type " + type, MessageType.SERVER_VERSION, type); assertEquals("Wrong type " + type, MessageType.SERVER_VERSION, type);
final long major = response.getServerVersion().getMajor(); 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<TwoWayChannelMessage> initiateCapture) { private void assertExpireTime(long expectedExpire, Capture<TwoWayChannelMessage> initiateCapture) {
@ -136,12 +150,12 @@ public class PaymentChannelServerTest {
} }
private TwoWayChannelMessage createClientVersionMessage() { 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(); return TwoWayChannelMessage.newBuilder().setType(MessageType.CLIENT_VERSION).setClientVersion(clientVersion).build();
} }
private TwoWayChannelMessage createClientVersionMessage(long timeWindow) { 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); if (timeWindow > 0) clientVersion.setTimeWindowSecs(timeWindow);
return TwoWayChannelMessage.newBuilder().setType(MessageType.CLIENT_VERSION).setClientVersion(clientVersion).build(); return TwoWayChannelMessage.newBuilder().setType(MessageType.CLIENT_VERSION).setClientVersion(clientVersion).build();
} }

View File

@ -26,9 +26,12 @@ import com.google.common.util.concurrent.SettableFuture;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException; 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.bitcoinj.testing.FakeTxBuilder.makeSolvedTestBlock;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@RunWith(Parameterized.class)
public class PaymentChannelStateTest extends TestWithWallet { public class PaymentChannelStateTest extends TestWithWallet {
private ECKey serverKey; private ECKey serverKey;
private Coin halfCoin; private Coin halfCoin;
@ -48,6 +52,27 @@ public class PaymentChannelStateTest extends TestWithWallet {
private TransactionBroadcaster mockBroadcaster; private TransactionBroadcaster mockBroadcaster;
private BlockingQueue<TxFuturePair> broadcasts; private BlockingQueue<TxFuturePair> 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<PaymentChannelClient.VersionSelector> data() {
return Arrays.asList(
PaymentChannelClient.VersionSelector.VERSION_1,
PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1);
}
@Parameterized.Parameter
public PaymentChannelClient.VersionSelector versionSelector;
/**
* Returns <code>true</code> 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 { private static class TxFuturePair {
Transaction tx; Transaction tx;
SettableFuture<Transaction> future; SettableFuture<Transaction> future;
@ -61,6 +86,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
@Override @Override
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
Utils.setMockClock(); // Use mock clock
super.setUp(); super.setUp();
wallet.addExtension(new StoredPaymentChannelClientStates(wallet, new TransactionBroadcaster() { wallet.addExtension(new StoredPaymentChannelClientStates(wallet, new TransactionBroadcaster() {
@Override @Override
@ -93,13 +119,93 @@ public class PaymentChannelStateTest extends TestWithWallet {
super.tearDown(); 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 @Test
public void stateErrors() throws Exception { public void stateErrors() throws Exception {
PaymentChannelClientState channelState = new PaymentChannelClientState(wallet, myKey, serverKey, PaymentChannelClientState channelState = makeClientState(wallet, myKey, serverKey,
COIN.multiply(10), 20); COIN.multiply(10), 20);
assertEquals(PaymentChannelClientState.State.NEW, channelState.getState()); assertEquals(PaymentChannelClientState.State.NEW, channelState.getState());
try { try {
channelState.getMultisigContract(); channelState.getContract();
fail(); fail();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
// Expected. // Expected.
@ -117,36 +223,48 @@ public class PaymentChannelStateTest extends TestWithWallet {
Utils.setMockClock(); // Use mock clock Utils.setMockClock(); // Use mock clock
final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24; final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24;
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); 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()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
clientState.initiate(); 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. // Send the refund tx from client to server and get back the signature.
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); Transaction refund;
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); if (useRefunds()) {
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize());
// This verifies that the refund can spend the multi-sig output when run. byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
clientState.provideRefundSignature(refundSig, null); 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()); assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave(); clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
// Validate the multisig contract looks right. // 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(PaymentChannelClientState.State.READY, clientState.getState());
assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change.
Script script = multisigContract.getOutput(0).getScriptPubKey(); 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(); script = multisigContract.getOutput(1).getScriptPubKey();
assertTrue(script.isSentToAddress()); assertTrue(script.isSentToAddress());
assertTrue(wallet.getPendingTransactions().contains(multisigContract)); assertTrue(wallet.getPendingTransactions().contains(multisigContract));
// Provide the server with the multisig contract and simulate successful propagation/acceptance. // 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()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
final TxFuturePair pair = broadcasts.take(); final TxFuturePair pair = broadcasts.take();
pair.future.set(pair.tx); pair.future.set(pair.tx);
@ -156,12 +274,12 @@ public class PaymentChannelStateTest extends TestWithWallet {
assertEquals(2, wallet.getTransactions(false).size()); assertEquals(2, wallet.getTransactions(false).size());
Iterator<Transaction> walletTransactionIterator = wallet.getTransactions(false).iterator(); Iterator<Transaction> walletTransactionIterator = wallet.getTransactions(false).iterator();
Transaction clientWalletMultisigContract = walletTransactionIterator.next(); Transaction clientWalletMultisigContract = walletTransactionIterator.next();
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash()));
if (!clientWalletMultisigContract.getHash().equals(multisigContract.getHash())) { if (!clientWalletMultisigContract.getHash().equals(multisigContract.getHash())) {
clientWalletMultisigContract = walletTransactionIterator.next(); clientWalletMultisigContract = walletTransactionIterator.next();
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash()));
} else } else
assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getCompletedRefundTransaction().getHash())); assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getRefundTransaction().getHash()));
assertEquals(multisigContract.getHash(), clientWalletMultisigContract.getHash()); assertEquals(multisigContract.getHash(), clientWalletMultisigContract.getHash());
assertFalse(clientWalletMultisigContract.getInput(0).getConnectedOutput().getSpentBy().getParentTransaction().getHash().equals(refund.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 Utils.setMockClock(); // Use mock clock
final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24; final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24;
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); 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); CENT.divide(2), EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
assertEquals(CENT.divide(2), clientState.getTotalValue()); assertEquals(CENT.divide(2), clientState.getTotalValue());
clientState.initiate(); clientState.initiate();
// We will have to pay min_tx_fee twice - both the multisig contract and the refund tx // 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(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. if (useRefunds()) {
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); // Send the refund tx from client to server and get back the signature.
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); Transaction refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize());
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
// This verifies that the refund can spend the multi-sig output when run. assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
clientState.provideRefundSignature(refundSig, null); // 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()); assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave(); clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
// Validate the multisig contract looks right. // 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(PaymentChannelClientState.State.READY, clientState.getState());
assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change.
Script script = multisigContract.getOutput(0).getScriptPubKey(); 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(); script = multisigContract.getOutput(1).getScriptPubKey();
assertTrue(script.isSentToAddress()); assertTrue(script.isSentToAddress());
assertTrue(wallet.getPendingTransactions().contains(multisigContract)); assertTrue(wallet.getPendingTransactions().contains(multisigContract));
// Provide the server with the multisig contract and simulate successful propagation/acceptance. // 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()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
final TxFuturePair pop = broadcasts.take(); final TxFuturePair pop = broadcasts.take();
pop.future.set(pop.tx); pop.future.set(pop.tx);
@ -301,7 +428,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
clientBroadcastedMultiSig.future.set(clientBroadcastedMultiSig.tx); clientBroadcastedMultiSig.future.set(clientBroadcastedMultiSig.tx);
Transaction clientBroadcastedRefund = broadcastRefund.tx; Transaction clientBroadcastedRefund = broadcastRefund.tx;
assertEquals(clientBroadcastedRefund.getHash(), clientState.getCompletedRefundTransaction().getHash()); assertEquals(clientBroadcastedRefund.getHash(), clientState.getRefundTransaction().getHash());
for (TransactionInput input : clientBroadcastedRefund.getInputs()) { for (TransactionInput input : clientBroadcastedRefund.getInputs()) {
// If the multisig output is connected, the wallet will fail to deserialize // If the multisig output is connected, the wallet will fail to deserialize
if (input.getOutpoint().getHash().equals(clientBroadcastedMultiSig.tx.getHash())) if (input.getOutpoint().getHash().equals(clientBroadcastedMultiSig.tx.getHash()))
@ -332,93 +459,122 @@ public class PaymentChannelStateTest extends TestWithWallet {
Utils.setMockClock(); // Use mock clock Utils.setMockClock(); // Use mock clock
final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24; final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24;
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); 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()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
clientState.initiate(); clientState.initiate();
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState()); assertEquals(getInitialClientState(), clientState.getState());
// Test refund transaction with any number of issues if (useRefunds()) {
byte[] refundTxBytes = clientState.getIncompleteRefundTransaction().bitcoinSerialize(); // Test refund transaction with any number of issues
Transaction refund = new Transaction(params, refundTxBytes); byte[] refundTxBytes = clientV1State().getIncompleteRefundTransaction().bitcoinSerialize();
refund.addOutput(Coin.ZERO, new ECKey().toAddress(params)); Transaction refund = new Transaction(params, refundTxBytes);
try { refund.addOutput(Coin.ZERO, new ECKey().toAddress(params));
serverState.provideRefundTransaction(refund, myKey.getPubKey()); try {
fail(); serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
} catch (VerificationException e) {} fail();
} catch (VerificationException e) {
}
refund = new Transaction(params, refundTxBytes); refund = new Transaction(params, refundTxBytes);
refund.addInput(new TransactionInput(params, refund, new byte[] {}, new TransactionOutPoint(params, 42, refund.getHash()))); refund.addInput(new TransactionInput(params, refund, new byte[]{}, new TransactionOutPoint(params, 42, refund.getHash())));
try { try {
serverState.provideRefundTransaction(refund, myKey.getPubKey()); serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
fail(); fail();
} catch (VerificationException e) {} } catch (VerificationException e) {
}
refund = new Transaction(params, refundTxBytes); refund = new Transaction(params, refundTxBytes);
refund.setLockTime(0); refund.setLockTime(0);
try { try {
serverState.provideRefundTransaction(refund, myKey.getPubKey()); serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
fail(); fail();
} catch (VerificationException e) {} } catch (VerificationException e) {
}
refund = new Transaction(params, refundTxBytes); refund = new Transaction(params, refundTxBytes);
refund.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE); refund.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE);
try { try {
serverState.provideRefundTransaction(refund, myKey.getPubKey()); serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
fail(); fail();
} catch (VerificationException e) {} } catch (VerificationException e) {
}
refund = new Transaction(params, refundTxBytes); refund = new Transaction(params, refundTxBytes);
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
try { serverState.provideRefundTransaction(refund, myKey.getPubKey()); fail(); } catch (IllegalStateException e) {} try {
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); 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); byte[] refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
refundSigCopy[refundSigCopy.length-1] = (byte) (Transaction.SigHash.NONE.ordinal() + 1); refundSigCopy[refundSigCopy.length - 1] = (byte) (Transaction.SigHash.NONE.ordinal() + 1);
try { try {
clientState.provideRefundSignature(refundSigCopy, null); clientV1State().provideRefundSignature(refundSigCopy, null);
fail(); fail();
} catch (VerificationException e) { } catch (VerificationException e) {
assertTrue(e.getMessage().contains("SIGHASH_NONE")); 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()); assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave(); clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); 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) {} 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); Transaction multisigContract = new Transaction(params, multisigContractSerialized);
multisigContract.clearOutputs(); 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 { try {
serverState.provideMultiSigContract(multisigContract); serverState.provideContract(multisigContract);
fail(); fail();
} catch (VerificationException e) { } catch (VerificationException e) {
assertTrue(e.getMessage().contains("client and server in that order")); 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 = new Transaction(params, multisigContractSerialized);
multisigContract.clearOutputs(); 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 { try {
serverState.provideMultiSigContract(multisigContract); serverState.provideContract(multisigContract);
fail(); fail();
} catch (VerificationException e) { } catch (VerificationException e) {
assertTrue(e.getMessage().contains("zero value")); assertTrue(e.getMessage().contains("zero value"));
@ -438,13 +600,13 @@ public class PaymentChannelStateTest extends TestWithWallet {
multisigContract.clearOutputs(); multisigContract.clearOutputs();
multisigContract.addOutput(new TransactionOutput(params, multisigContract, halfCoin, new byte[] {0x01})); multisigContract.addOutput(new TransactionOutput(params, multisigContract, halfCoin, new byte[] {0x01}));
try { try {
serverState.provideMultiSigContract(multisigContract); serverState.provideContract(multisigContract);
fail(); fail();
} catch (VerificationException e) {} } catch (VerificationException e) {}
multisigContract = new Transaction(params, multisigContractSerialized); multisigContract = new Transaction(params, multisigContractSerialized);
ListenableFuture<PaymentChannelServerState> multisigStateFuture = serverState.provideMultiSigContract(multisigContract); ListenableFuture<PaymentChannelServerState> multisigStateFuture = serverState.provideContract(multisigContract);
try { serverState.provideMultiSigContract(multisigContract); fail(); } catch (IllegalStateException e) {} try { serverState.provideContract(multisigContract); fail(); } catch (IllegalStateException e) {}
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
assertFalse(multisigStateFuture.isDone()); assertFalse(multisigStateFuture.isDone());
final TxFuturePair pair = broadcasts.take(); final TxFuturePair pair = broadcasts.take();
@ -536,18 +698,18 @@ public class PaymentChannelStateTest extends TestWithWallet {
Utils.setMockClock(); // Use mock clock Utils.setMockClock(); // Use mock clock
final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24; final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24;
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); assertEquals(getInitialServerState(), serverState.getState());
// Clearly SATOSHI is far too small to be useful // 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()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
try { try {
clientState.initiate(); clientState.initiate();
fail(); fail();
} catch (ValueOutOfRangeException e) {} } 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), Transaction.MIN_NONDUST_OUTPUT.subtract(Coin.SATOSHI).add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE),
EXPIRE_TIME); EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
@ -557,37 +719,42 @@ public class PaymentChannelStateTest extends TestWithWallet {
} catch (ValueOutOfRangeException e) {} } catch (ValueOutOfRangeException e) {}
// Verify that MIN_NONDUST_OUTPUT + MIN_TX_FEE is accepted // 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); Transaction.MIN_NONDUST_OUTPUT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), EXPIRE_TIME);
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState()); 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... // We'll have to pay REFERENCE_DEFAULT_MIN_TX_FEE twice (multisig+refund), and we'll end up getting back nearly nothing...
clientState.initiate(); clientState.initiate();
assertEquals(clientState.getRefundTxFees(), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(2)); 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 // 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()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
clientState.initiate(); clientState.initiate();
assertEquals(clientState.getRefundTxFees(), Coin.ZERO); 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. if (useRefunds()) {
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); // Send the refund tx from client to server and get back the signature.
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); Transaction refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize());
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
// This verifies that the refund can spend the multi-sig output when run. assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
clientState.provideRefundSignature(refundSig, null); // 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()); assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave(); clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
// Get the multisig contract // 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()); assertEquals(PaymentChannelClientState.State.READY, clientState.getState());
// Provide the server with the multisig contract and simulate successful propagation/acceptance. // 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()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
TxFuturePair pair = broadcasts.take(); TxFuturePair pair = broadcasts.take();
pair.future.set(pair.tx); pair.future.set(pair.tx);
@ -639,41 +806,63 @@ public class PaymentChannelStateTest extends TestWithWallet {
Utils.setMockClock(); // Use mock clock Utils.setMockClock(); // Use mock clock
final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24; final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24;
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); assertEquals(getInitialServerState(), serverState.getState());
clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) { switch (versionSelector) {
@Override case VERSION_1:
protected void editContractSendRequest(Wallet.SendRequest req) { clientState = new PaymentChannelV1ClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) {
req.coinSelector = wallet.getCoinSelector(); @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()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
clientState.initiate(); 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. if (useRefunds()) {
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); // Send the refund tx from client to server and get back the signature.
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); Transaction refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize());
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
// This verifies that the refund can spend the multi-sig output when run. assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
clientState.provideRefundSignature(refundSig, null); // 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()); assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave(); clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
// Validate the multisig contract looks right. // 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(PaymentChannelV1ClientState.State.READY, clientState.getState());
assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change.
Script script = multisigContract.getOutput(0).getScriptPubKey(); 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(); script = multisigContract.getOutput(1).getScriptPubKey();
assertTrue(script.isSentToAddress()); assertTrue(script.isSentToAddress());
assertTrue(wallet.getPendingTransactions().contains(multisigContract)); assertTrue(wallet.getPendingTransactions().contains(multisigContract));
// Provide the server with the multisig contract and simulate successful propagation/acceptance. // 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()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
TxFuturePair pair = broadcasts.take(); TxFuturePair pair = broadcasts.take();
pair.future.set(pair.tx); pair.future.set(pair.tx);
@ -725,36 +914,48 @@ public class PaymentChannelStateTest extends TestWithWallet {
Utils.setMockClock(); // Use mock clock Utils.setMockClock(); // Use mock clock
final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24; final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24;
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME); serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState()); 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()); assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
clientState.initiate(); 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;
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize()); if (useRefunds()) {
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey()); refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize());
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState()); // Send the refund tx from client to server and get back the signature.
// This verifies that the refund can spend the multi-sig output when run. byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
clientState.provideRefundSignature(refundSig, null); 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()); assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave(); clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState()); assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
// Validate the multisig contract looks right. // 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(PaymentChannelClientState.State.READY, clientState.getState());
assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change. assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change.
Script script = multisigContract.getOutput(0).getScriptPubKey(); 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(); script = multisigContract.getOutput(1).getScriptPubKey();
assertTrue(script.isSentToAddress()); assertTrue(script.isSentToAddress());
assertTrue(wallet.getPendingTransactions().contains(multisigContract)); assertTrue(wallet.getPendingTransactions().contains(multisigContract));
// Provide the server with the multisig contract and simulate successful propagation/acceptance. // 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()); assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
final TxFuturePair pair = broadcasts.take(); final TxFuturePair pair = broadcasts.take();
pair.future.set(pair.tx); pair.future.set(pair.tx);
@ -764,12 +965,12 @@ public class PaymentChannelStateTest extends TestWithWallet {
assertEquals(2, wallet.getTransactions(false).size()); assertEquals(2, wallet.getTransactions(false).size());
Iterator<Transaction> walletTransactionIterator = wallet.getTransactions(false).iterator(); Iterator<Transaction> walletTransactionIterator = wallet.getTransactions(false).iterator();
Transaction clientWalletMultisigContract = walletTransactionIterator.next(); Transaction clientWalletMultisigContract = walletTransactionIterator.next();
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash()));
if (!clientWalletMultisigContract.getHash().equals(multisigContract.getHash())) { if (!clientWalletMultisigContract.getHash().equals(multisigContract.getHash())) {
clientWalletMultisigContract = walletTransactionIterator.next(); clientWalletMultisigContract = walletTransactionIterator.next();
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash())); assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash()));
} else } else
assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getCompletedRefundTransaction().getHash())); assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getRefundTransaction().getHash()));
assertEquals(multisigContract.getHash(), clientWalletMultisigContract.getHash()); assertEquals(multisigContract.getHash(), clientWalletMultisigContract.getHash());
assertFalse(clientWalletMultisigContract.getInput(0).getConnectedOutput().getSpentBy().getParentTransaction().getHash().equals(refund.getHash())); assertFalse(clientWalletMultisigContract.getInput(0).getConnectedOutput().getSpentBy().getParentTransaction().getHash().equals(refund.getHash()));

View File

@ -20,6 +20,7 @@ package org.bitcoinj.examples;
import org.bitcoinj.core.*; import org.bitcoinj.core.*;
import org.bitcoinj.kits.WalletAppKit; import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.protocols.channels.PaymentChannelClient;
import org.bitcoinj.protocols.channels.PaymentChannelClientConnection; import org.bitcoinj.protocols.channels.PaymentChannelClientConnection;
import org.bitcoinj.protocols.channels.StoredPaymentChannelClientStates; import org.bitcoinj.protocols.channels.StoredPaymentChannelClientStates;
import org.bitcoinj.protocols.channels.ValueOutOfRangeException; 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 { 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( 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. // Opening the channel requires talking to the server, so it's asynchronous.
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
Futures.addCallback(client.getChannelOpenFuture(), new FutureCallback<PaymentChannelClientConnection>() { Futures.addCallback(client.getChannelOpenFuture(), new FutureCallback<PaymentChannelClientConnection>() {

View File

@ -97,10 +97,10 @@ public class ExamplePaymentChannelServer implements PaymentChannelServerListener
log.info(" with a maximum value of {}, expiring at UNIX timestamp {}.", 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 // The channel's maximum value is the value of the multisig contract which locks in some
// amount of money to the channel // 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 // The channel expires at some offset from when the client's refund transaction becomes
// spendable. // spendable.
state.getRefundTransactionUnlockTime() + StoredPaymentChannelServerStates.CHANNEL_EXPIRE_OFFSET); state.getExpiryTime() + StoredPaymentChannelServerStates.CHANNEL_EXPIRE_OFFSET);
} }
@Override @Override

View File

@ -724,7 +724,7 @@ public class WalletTool {
throw new RuntimeException(e); 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())) { 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"); log.info("Emptying out wallet, recipient may get less than what you expect");
req.emptyWallet = true; req.emptyWallet = true;