mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-13 02: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:
parent
4b2afc9667
commit
8af0fa9884
@ -6006,11 +6006,17 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The serialized bytes of the transaction in Satoshi format.
|
||||
* For version 1:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a 2-of-2 multisig output with the first pubkey being the
|
||||
* primary's and the second being the secondary's (ie the script must be exactly "OP_2
|
||||
* ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG")
|
||||
* For version 2:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the
|
||||
* primary's and the second being the secondary's.
|
||||
* </pre>
|
||||
*/
|
||||
boolean hasTx();
|
||||
@ -6019,11 +6025,17 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The serialized bytes of the transaction in Satoshi format.
|
||||
* For version 1:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a 2-of-2 multisig output with the first pubkey being the
|
||||
* primary's and the second being the secondary's (ie the script must be exactly "OP_2
|
||||
* ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG")
|
||||
* For version 2:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the
|
||||
* primary's and the second being the secondary's.
|
||||
* </pre>
|
||||
*/
|
||||
com.google.protobuf.ByteString getTx();
|
||||
@ -6061,6 +6073,29 @@ public final class Protos {
|
||||
* </pre>
|
||||
*/
|
||||
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}
|
||||
@ -6136,6 +6171,11 @@ public final class Protos {
|
||||
bitField0_ |= 0x00000002;
|
||||
break;
|
||||
}
|
||||
case 26: {
|
||||
bitField0_ |= 0x00000004;
|
||||
clientKey_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
@ -6183,11 +6223,17 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The serialized bytes of the transaction in Satoshi format.
|
||||
* For version 1:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a 2-of-2 multisig output with the first pubkey being the
|
||||
* primary's and the second being the secondary's (ie the script must be exactly "OP_2
|
||||
* ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG")
|
||||
* For version 2:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the
|
||||
* primary's and the second being the secondary's.
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasTx() {
|
||||
@ -6198,11 +6244,17 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The serialized bytes of the transaction in Satoshi format.
|
||||
* For version 1:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a 2-of-2 multisig output with the first pubkey being the
|
||||
* primary's and the second being the secondary's (ie the script must be exactly "OP_2
|
||||
* ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG")
|
||||
* For version 2:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the
|
||||
* primary's and the second being the secondary's.
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getTx() {
|
||||
@ -6251,9 +6303,39 @@ public final class Protos {
|
||||
return initialPayment_;
|
||||
}
|
||||
|
||||
public static final int CLIENT_KEY_FIELD_NUMBER = 3;
|
||||
private com.google.protobuf.ByteString clientKey_;
|
||||
/**
|
||||
* <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() {
|
||||
tx_ = com.google.protobuf.ByteString.EMPTY;
|
||||
initialPayment_ = org.bitcoin.paymentchannel.Protos.UpdatePayment.getDefaultInstance();
|
||||
clientKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
}
|
||||
private byte memoizedIsInitialized = -1;
|
||||
public final boolean isInitialized() {
|
||||
@ -6286,6 +6368,9 @@ public final class Protos {
|
||||
if (((bitField0_ & 0x00000002) == 0x00000002)) {
|
||||
output.writeMessage(2, initialPayment_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
output.writeBytes(3, clientKey_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@ -6303,6 +6388,10 @@ public final class Protos {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeMessageSize(2, initialPayment_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(3, clientKey_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@ -6433,6 +6522,8 @@ public final class Protos {
|
||||
initialPaymentBuilder_.clear();
|
||||
}
|
||||
bitField0_ = (bitField0_ & ~0x00000002);
|
||||
clientKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000004);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -6473,6 +6564,10 @@ public final class Protos {
|
||||
} else {
|
||||
result.initialPayment_ = initialPaymentBuilder_.build();
|
||||
}
|
||||
if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
to_bitField0_ |= 0x00000004;
|
||||
}
|
||||
result.clientKey_ = clientKey_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
@ -6495,6 +6590,9 @@ public final class Protos {
|
||||
if (other.hasInitialPayment()) {
|
||||
mergeInitialPayment(other.getInitialPayment());
|
||||
}
|
||||
if (other.hasClientKey()) {
|
||||
setClientKey(other.getClientKey());
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
return this;
|
||||
}
|
||||
@ -6540,11 +6638,17 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The serialized bytes of the transaction in Satoshi format.
|
||||
* For version 1:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a 2-of-2 multisig output with the first pubkey being the
|
||||
* primary's and the second being the secondary's (ie the script must be exactly "OP_2
|
||||
* ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG")
|
||||
* For version 2:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the
|
||||
* primary's and the second being the secondary's.
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasTx() {
|
||||
@ -6555,11 +6659,17 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The serialized bytes of the transaction in Satoshi format.
|
||||
* For version 1:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a 2-of-2 multisig output with the first pubkey being the
|
||||
* primary's and the second being the secondary's (ie the script must be exactly "OP_2
|
||||
* ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG")
|
||||
* For version 2:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the
|
||||
* primary's and the second being the secondary's.
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getTx() {
|
||||
@ -6570,11 +6680,17 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The serialized bytes of the transaction in Satoshi format.
|
||||
* For version 1:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a 2-of-2 multisig output with the first pubkey being the
|
||||
* primary's and the second being the secondary's (ie the script must be exactly "OP_2
|
||||
* ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG")
|
||||
* For version 2:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the
|
||||
* primary's and the second being the secondary's.
|
||||
* </pre>
|
||||
*/
|
||||
public Builder setTx(com.google.protobuf.ByteString value) {
|
||||
@ -6591,11 +6707,17 @@ public final class Protos {
|
||||
*
|
||||
* <pre>
|
||||
* The serialized bytes of the transaction in Satoshi format.
|
||||
* For version 1:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a 2-of-2 multisig output with the first pubkey being the
|
||||
* primary's and the second being the secondary's (ie the script must be exactly "OP_2
|
||||
* ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG")
|
||||
* For version 2:
|
||||
* * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
* necessary fees) TODO: tell the client how much fee it needs
|
||||
* * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the
|
||||
* primary's and the second being the secondary's.
|
||||
* </pre>
|
||||
*/
|
||||
public Builder clearTx() {
|
||||
@ -6784,6 +6906,69 @@ public final class Protos {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -9439,21 +9624,22 @@ public final class Protos {
|
||||
"d_channel_size\030\002 \002(\004\022\030\n\020expire_time_secs" +
|
||||
"\030\003 \002(\004\022\023\n\013min_payment\030\004 \002(\004\"1\n\rProvideRe" +
|
||||
"fund\022\024\n\014multisig_key\030\001 \002(\014\022\n\n\002tx\030\002 \002(\014\"!",
|
||||
"\n\014ReturnRefund\022\021\n\tsignature\030\001 \002(\014\"V\n\017Pro" +
|
||||
"\n\014ReturnRefund\022\021\n\tsignature\030\001 \002(\014\"j\n\017Pro" +
|
||||
"videContract\022\n\n\002tx\030\001 \002(\014\0227\n\017initial_paym" +
|
||||
"ent\030\002 \002(\0132\036.paymentchannels.UpdatePaymen" +
|
||||
"t\"M\n\rUpdatePayment\022\033\n\023client_change_valu" +
|
||||
"e\030\001 \002(\004\022\021\n\tsignature\030\002 \002(\014\022\014\n\004info\030\003 \001(\014" +
|
||||
"\"\032\n\nPaymentAck\022\014\n\004info\030\001 \001(\014\"\030\n\nSettleme" +
|
||||
"nt\022\n\n\002tx\030\003 \002(\014\"\251\002\n\005Error\0225\n\004code\030\001 \001(\0162 " +
|
||||
".paymentchannels.Error.ErrorCode:\005OTHER\022" +
|
||||
"\023\n\013explanation\030\002 \001(\t\022\026\n\016expected_value\030\003" +
|
||||
" \001(\004\"\273\001\n\tErrorCode\022\013\n\007TIMEOUT\020\001\022\020\n\014SYNTA",
|
||||
"X_ERROR\020\002\022\031\n\025NO_ACCEPTABLE_VERSION\020\003\022\023\n\017" +
|
||||
"BAD_TRANSACTION\020\004\022\034\n\030TIME_WINDOW_UNACCEP" +
|
||||
"TABLE\020\005\022\033\n\027CHANNEL_VALUE_TOO_LARGE\020\006\022\031\n\025" +
|
||||
"MIN_PAYMENT_TOO_LARGE\020\007\022\t\n\005OTHER\020\010B$\n\032or" +
|
||||
"g.bitcoin.paymentchannelB\006Protos"
|
||||
"t\022\022\n\nclient_key\030\003 \001(\014\"M\n\rUpdatePayment\022\033" +
|
||||
"\n\023client_change_value\030\001 \002(\004\022\021\n\tsignature" +
|
||||
"\030\002 \002(\014\022\014\n\004info\030\003 \001(\014\"\032\n\nPaymentAck\022\014\n\004in" +
|
||||
"fo\030\001 \001(\014\"\030\n\nSettlement\022\n\n\002tx\030\003 \002(\014\"\251\002\n\005E" +
|
||||
"rror\0225\n\004code\030\001 \001(\0162 .paymentchannels.Err" +
|
||||
"or.ErrorCode:\005OTHER\022\023\n\013explanation\030\002 \001(\t" +
|
||||
"\022\026\n\016expected_value\030\003 \001(\004\"\273\001\n\tErrorCode\022\013",
|
||||
"\n\007TIMEOUT\020\001\022\020\n\014SYNTAX_ERROR\020\002\022\031\n\025NO_ACCE" +
|
||||
"PTABLE_VERSION\020\003\022\023\n\017BAD_TRANSACTION\020\004\022\034\n" +
|
||||
"\030TIME_WINDOW_UNACCEPTABLE\020\005\022\033\n\027CHANNEL_V" +
|
||||
"ALUE_TOO_LARGE\020\006\022\031\n\025MIN_PAYMENT_TOO_LARG" +
|
||||
"E\020\007\022\t\n\005OTHER\020\010B$\n\032org.bitcoin.paymentcha" +
|
||||
"nnelB\006Protos"
|
||||
};
|
||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||
new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() {
|
||||
@ -9508,7 +9694,7 @@ public final class Protos {
|
||||
internal_static_paymentchannels_ProvideContract_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_paymentchannels_ProvideContract_descriptor,
|
||||
new java.lang.String[] { "Tx", "InitialPayment", });
|
||||
new java.lang.String[] { "Tx", "InitialPayment", "ClientKey", });
|
||||
internal_static_paymentchannels_UpdatePayment_descriptor =
|
||||
getDescriptor().getMessageTypes().get(7);
|
||||
internal_static_paymentchannels_UpdatePayment_fieldAccessorTable = new
|
||||
|
@ -3818,11 +3818,12 @@ public class Wallet extends BaseTaggableObject
|
||||
return toCLTVPaymentChannel(params, BigInteger.valueOf(time), from, to, value);
|
||||
}
|
||||
|
||||
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, long lockTime, ECKey from, ECKey to, Coin value) {
|
||||
return toCLTVPaymentChannel(params, BigInteger.valueOf(lockTime), from, to, value);
|
||||
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, int releaseBlock, ECKey from, ECKey to, Coin value) {
|
||||
checkArgument(0 <= releaseBlock && releaseBlock < Transaction.LOCKTIME_THRESHOLD, "Block number was too large");
|
||||
return toCLTVPaymentChannel(params, BigInteger.valueOf(releaseBlock), from, to, value);
|
||||
}
|
||||
|
||||
private static SendRequest toCLTVPaymentChannel(NetworkParameters params, BigInteger time, ECKey from, ECKey to, Coin value) {
|
||||
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, BigInteger time, ECKey from, ECKey to, Coin value) {
|
||||
SendRequest req = new SendRequest();
|
||||
Script output = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to);
|
||||
req.tx = new Transaction(params);
|
||||
@ -4223,6 +4224,7 @@ public class Wallet extends BaseTaggableObject
|
||||
log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i);
|
||||
continue;
|
||||
} catch (ScriptException e) {
|
||||
log.debug("Input contained an incorrect signature", e);
|
||||
// Expected.
|
||||
}
|
||||
|
||||
|
@ -772,10 +772,18 @@ public final class ClientState {
|
||||
|
||||
/**
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Fees required to refund the transaction.
|
||||
* </pre>
|
||||
*/
|
||||
boolean hasRefundFees();
|
||||
/**
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Fees required to refund the transaction.
|
||||
* </pre>
|
||||
*/
|
||||
long getRefundFees();
|
||||
|
||||
@ -799,6 +807,49 @@ public final class ClientState {
|
||||
* </pre>
|
||||
*/
|
||||
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}
|
||||
@ -897,6 +948,21 @@ public final class ClientState {
|
||||
myPublicKey_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
case 72: {
|
||||
bitField0_ |= 0x00000100;
|
||||
majorVersion_ = input.readUInt32();
|
||||
break;
|
||||
}
|
||||
case 80: {
|
||||
bitField0_ |= 0x00000200;
|
||||
expiryTime_ = input.readUInt64();
|
||||
break;
|
||||
}
|
||||
case 90: {
|
||||
bitField0_ |= 0x00000400;
|
||||
serverKey_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
@ -1039,12 +1105,20 @@ public final class ClientState {
|
||||
private long refundFees_;
|
||||
/**
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Fees required to refund the transaction.
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasRefundFees() {
|
||||
return ((bitField0_ & 0x00000040) == 0x00000040);
|
||||
}
|
||||
/**
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Fees required to refund the transaction.
|
||||
* </pre>
|
||||
*/
|
||||
public long getRefundFees() {
|
||||
return refundFees_;
|
||||
@ -1077,6 +1151,67 @@ public final class ClientState {
|
||||
return closeTransactionHash_;
|
||||
}
|
||||
|
||||
public static final int MAJORVERSION_FIELD_NUMBER = 9;
|
||||
private int majorVersion_;
|
||||
/**
|
||||
* <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() {
|
||||
id_ = com.google.protobuf.ByteString.EMPTY;
|
||||
contractTransaction_ = com.google.protobuf.ByteString.EMPTY;
|
||||
@ -1086,6 +1221,9 @@ public final class ClientState {
|
||||
valueToMe_ = 0L;
|
||||
refundFees_ = 0L;
|
||||
closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
|
||||
majorVersion_ = 1;
|
||||
expiryTime_ = 0L;
|
||||
serverKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
}
|
||||
private byte memoizedIsInitialized = -1;
|
||||
public final boolean isInitialized() {
|
||||
@ -1152,6 +1290,15 @@ public final class ClientState {
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
output.writeBytes(8, myPublicKey_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000100) == 0x00000100)) {
|
||||
output.writeUInt32(9, majorVersion_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000200) == 0x00000200)) {
|
||||
output.writeUInt64(10, expiryTime_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000400) == 0x00000400)) {
|
||||
output.writeBytes(11, serverKey_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@ -1193,6 +1340,18 @@ public final class ClientState {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(8, myPublicKey_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000100) == 0x00000100)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt32Size(9, majorVersion_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000200) == 0x00000200)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt64Size(10, expiryTime_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000400) == 0x00000400)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(11, serverKey_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@ -1331,6 +1490,12 @@ public final class ClientState {
|
||||
bitField0_ = (bitField0_ & ~0x00000040);
|
||||
closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000080);
|
||||
majorVersion_ = 1;
|
||||
bitField0_ = (bitField0_ & ~0x00000100);
|
||||
expiryTime_ = 0L;
|
||||
bitField0_ = (bitField0_ & ~0x00000200);
|
||||
serverKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000400);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -1391,6 +1556,18 @@ public final class ClientState {
|
||||
to_bitField0_ |= 0x00000080;
|
||||
}
|
||||
result.closeTransactionHash_ = closeTransactionHash_;
|
||||
if (((from_bitField0_ & 0x00000100) == 0x00000100)) {
|
||||
to_bitField0_ |= 0x00000100;
|
||||
}
|
||||
result.majorVersion_ = majorVersion_;
|
||||
if (((from_bitField0_ & 0x00000200) == 0x00000200)) {
|
||||
to_bitField0_ |= 0x00000200;
|
||||
}
|
||||
result.expiryTime_ = expiryTime_;
|
||||
if (((from_bitField0_ & 0x00000400) == 0x00000400)) {
|
||||
to_bitField0_ |= 0x00000400;
|
||||
}
|
||||
result.serverKey_ = serverKey_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
@ -1431,6 +1608,15 @@ public final class ClientState {
|
||||
if (other.hasCloseTransactionHash()) {
|
||||
setCloseTransactionHash(other.getCloseTransactionHash());
|
||||
}
|
||||
if (other.hasMajorVersion()) {
|
||||
setMajorVersion(other.getMajorVersion());
|
||||
}
|
||||
if (other.hasExpiryTime()) {
|
||||
setExpiryTime(other.getExpiryTime());
|
||||
}
|
||||
if (other.hasServerKey()) {
|
||||
setServerKey(other.getServerKey());
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
return this;
|
||||
}
|
||||
@ -1712,18 +1898,30 @@ public final class ClientState {
|
||||
private long refundFees_ ;
|
||||
/**
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Fees required to refund the transaction.
|
||||
* </pre>
|
||||
*/
|
||||
public boolean hasRefundFees() {
|
||||
return ((bitField0_ & 0x00000040) == 0x00000040);
|
||||
}
|
||||
/**
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Fees required to refund the transaction.
|
||||
* </pre>
|
||||
*/
|
||||
public long getRefundFees() {
|
||||
return refundFees_;
|
||||
}
|
||||
/**
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Fees required to refund the transaction.
|
||||
* </pre>
|
||||
*/
|
||||
public Builder setRefundFees(long value) {
|
||||
bitField0_ |= 0x00000040;
|
||||
@ -1733,6 +1931,10 @@ public final class ClientState {
|
||||
}
|
||||
/**
|
||||
* <code>required uint64 refundFees = 6;</code>
|
||||
*
|
||||
* <pre>
|
||||
* Fees required to refund the transaction.
|
||||
* </pre>
|
||||
*/
|
||||
public Builder clearRefundFees() {
|
||||
bitField0_ = (bitField0_ & ~0x00000040);
|
||||
@ -1800,6 +2002,137 @@ public final class ClientState {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -1833,13 +2166,15 @@ public final class ClientState {
|
||||
"\n storedclientpaymentchannel.proto\022\017paym" +
|
||||
"entchannels\"\\\n\033StoredClientPaymentChanne" +
|
||||
"ls\022=\n\010channels\030\001 \003(\0132+.paymentchannels.S" +
|
||||
"toredClientPaymentChannel\"\311\001\n\032StoredClie" +
|
||||
"toredClientPaymentChannel\"\211\002\n\032StoredClie" +
|
||||
"ntPaymentChannel\022\n\n\002id\030\001 \002(\014\022\033\n\023contract" +
|
||||
"Transaction\030\002 \002(\014\022\031\n\021refundTransaction\030\003" +
|
||||
" \002(\014\022\023\n\013myPublicKey\030\010 \002(\014\022\r\n\005myKey\030\004 \002(\014" +
|
||||
"\022\021\n\tvalueToMe\030\005 \002(\004\022\022\n\nrefundFees\030\006 \002(\004\022" +
|
||||
"\034\n\024closeTransactionHash\030\007 \001(\014B.\n\037org.bit" +
|
||||
"coinj.protocols.channelsB\013ClientState"
|
||||
"\034\n\024closeTransactionHash\030\007 \001(\014\022\027\n\014majorVe" +
|
||||
"rsion\030\t \001(\r:\0011\022\022\n\nexpiryTime\030\n \001(\004\022\021\n\tse",
|
||||
"rverKey\030\013 \001(\014B.\n\037org.bitcoinj.protocols." +
|
||||
"channelsB\013ClientState"
|
||||
};
|
||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||
new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() {
|
||||
@ -1864,7 +2199,7 @@ public final class ClientState {
|
||||
internal_static_paymentchannels_StoredClientPaymentChannel_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_paymentchannels_StoredClientPaymentChannel_descriptor,
|
||||
new java.lang.String[] { "Id", "ContractTransaction", "RefundTransaction", "MyPublicKey", "MyKey", "ValueToMe", "RefundFees", "CloseTransactionHash", });
|
||||
new java.lang.String[] { "Id", "ContractTransaction", "RefundTransaction", "MyPublicKey", "MyKey", "ValueToMe", "RefundFees", "CloseTransactionHash", "MajorVersion", "ExpiryTime", "ServerKey", });
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(outer_class_scope)
|
||||
|
@ -52,12 +52,12 @@ import static com.google.common.base.Preconditions.checkState;
|
||||
*/
|
||||
public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelClient.class);
|
||||
private static final int CLIENT_MAJOR_VERSION = 1;
|
||||
public final int CLIENT_MINOR_VERSION = 0;
|
||||
private static final int SERVER_MAJOR_VERSION = 1;
|
||||
|
||||
protected final ReentrantLock lock = Threading.lock("channelclient");
|
||||
|
||||
// Used to track the negotiated version number
|
||||
@GuardedBy("lock") private int majorVersion;
|
||||
|
||||
@GuardedBy("lock") private final ClientConnection conn;
|
||||
|
||||
// Used to keep track of whether or not the "socket" ie connection is open and we can generate messages
|
||||
@ -79,6 +79,42 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
}
|
||||
@GuardedBy("lock") private InitStep step = InitStep.WAITING_FOR_CONNECTION_OPEN;
|
||||
|
||||
public enum VersionSelector {
|
||||
VERSION_1,
|
||||
VERSION_2_ALLOW_1,
|
||||
VERSION_2;
|
||||
|
||||
public int getRequestedMajorVersion() {
|
||||
switch (this) {
|
||||
case VERSION_1:
|
||||
return 1;
|
||||
case VERSION_2_ALLOW_1:
|
||||
case VERSION_2:
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
public int getRequestedMinorVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean isServerVersionAccepted(int major, int minor) {
|
||||
switch (this) {
|
||||
case VERSION_1:
|
||||
return major == 1;
|
||||
case VERSION_2_ALLOW_1:
|
||||
return major == 1 || major == 2;
|
||||
case VERSION_2:
|
||||
return major == 2;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final VersionSelector versionSelector;
|
||||
|
||||
// Will either hold the StoredClientChannel of this channel or null after connectionOpen
|
||||
private StoredClientChannel storedChannel;
|
||||
// An arbitrary hash which identifies this channel (specified by the API user)
|
||||
@ -129,8 +165,36 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
* @param conn A callback listener which represents the connection to the server (forwards messages we generate to
|
||||
* the server)
|
||||
*/
|
||||
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, ClientConnection conn) {
|
||||
this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, null, conn);
|
||||
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId,
|
||||
ClientConnection conn) {
|
||||
this(wallet,myKey,maxValue,serverId, conn, VersionSelector.VERSION_2_ALLOW_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new channel manager which waits for {@link PaymentChannelClient#connectionOpen()} before acting.
|
||||
* A default time window of {@link #DEFAULT_TIME_WINDOW} will be used.
|
||||
*
|
||||
* @param wallet The wallet which will be paid from, and where completed transactions will be committed.
|
||||
* Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
|
||||
* @param maxValue The maximum value the server is allowed to request that we lock into this channel until the
|
||||
* refund transaction unlocks. Note that if there is a previously open channel, the refund
|
||||
* transaction used in this channel may be larger than maxValue. Thus, maxValue is not a method for
|
||||
* limiting the amount payable through this channel.
|
||||
* @param serverId An arbitrary hash representing this channel. This must uniquely identify the server. If an
|
||||
* existing stored channel exists in the wallet's {@link StoredPaymentChannelClientStates}, then an
|
||||
* attempt will be made to resume that channel.
|
||||
* @param conn A callback listener which represents the connection to the server (forwards messages we generate to
|
||||
* the server)
|
||||
* @param versionSelector An enum indicating which versions to support:
|
||||
* VERSION_1: use only version 1 of the protocol
|
||||
* VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1
|
||||
* VERSION_2: suggest version 2 and enforce use of version 2
|
||||
*
|
||||
*/
|
||||
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId,
|
||||
ClientConnection conn, VersionSelector versionSelector) {
|
||||
this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, null, conn, versionSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -155,6 +219,35 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
*/
|
||||
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, long timeWindow,
|
||||
@Nullable KeyParameter userKeySetup, ClientConnection conn) {
|
||||
this(wallet, myKey, maxValue, serverId, timeWindow, userKeySetup, conn, VersionSelector.VERSION_2_ALLOW_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new channel manager which waits for {@link PaymentChannelClient#connectionOpen()} before acting.
|
||||
*
|
||||
* @param wallet The wallet which will be paid from, and where completed transactions will be committed.
|
||||
* Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
|
||||
* @param maxValue The maximum value the server is allowed to request that we lock into this channel until the
|
||||
* refund transaction unlocks. Note that if there is a previously open channel, the refund
|
||||
* transaction used in this channel may be larger than maxValue. Thus, maxValue is not a method for
|
||||
* limiting the amount payable through this channel.
|
||||
* @param serverId An arbitrary hash representing this channel. This must uniquely identify the server. If an
|
||||
* existing stored channel exists in the wallet's {@link StoredPaymentChannelClientStates}, then an
|
||||
* attempt will be made to resume that channel.
|
||||
* @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open. Note that is is
|
||||
* a proposal to the server. The server may in turn propose something different.
|
||||
* See {@link org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientConnection#acceptExpireTime(long)}
|
||||
* @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup.
|
||||
* @param conn A callback listener which represents the connection to the server (forwards messages we generate to
|
||||
* the server)
|
||||
* @param versionSelector An enum indicating which versions to support:
|
||||
* VERSION_1: use only version 1 of the protocol
|
||||
* VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1
|
||||
* VERSION_2: suggest version 2 and enforce use of version 2
|
||||
*/
|
||||
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, long timeWindow,
|
||||
@Nullable KeyParameter userKeySetup, ClientConnection conn, VersionSelector versionSelector) {
|
||||
this.wallet = checkNotNull(wallet);
|
||||
this.myKey = checkNotNull(myKey);
|
||||
this.maxValue = checkNotNull(maxValue);
|
||||
@ -163,6 +256,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
this.timeWindow = timeWindow;
|
||||
this.conn = checkNotNull(conn);
|
||||
this.userKeySetup = userKeySetup;
|
||||
this.versionSelector = versionSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,7 +309,16 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
final byte[] pubKeyBytes = initiate.getMultisigKey().toByteArray();
|
||||
if (!ECKey.isPubKeyCanonical(pubKeyBytes))
|
||||
throw new VerificationException("Server gave us a non-canonical public key, protocol error.");
|
||||
state = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes), contractValue, expireTime);
|
||||
switch (majorVersion) {
|
||||
case 1:
|
||||
state = new PaymentChannelV1ClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes), contractValue, expireTime);
|
||||
break;
|
||||
case 2:
|
||||
state = new PaymentChannelV2ClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes), contractValue, expireTime);
|
||||
break;
|
||||
default:
|
||||
return CloseReason.NO_ACCEPTABLE_VERSION;
|
||||
}
|
||||
try {
|
||||
state.initiate(userKeySetup);
|
||||
} catch (ValueOutOfRangeException e) {
|
||||
@ -224,25 +327,62 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
return CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE;
|
||||
}
|
||||
minPayment = initiate.getMinPayment();
|
||||
step = InitStep.WAITING_FOR_REFUND_RETURN;
|
||||
switch (majorVersion) {
|
||||
case 1:
|
||||
step = InitStep.WAITING_FOR_REFUND_RETURN;
|
||||
|
||||
Protos.ProvideRefund.Builder provideRefundBuilder = Protos.ProvideRefund.newBuilder()
|
||||
.setMultisigKey(ByteString.copyFrom(myKey.getPubKey()))
|
||||
.setTx(ByteString.copyFrom(state.getIncompleteRefundTransaction().bitcoinSerialize()));
|
||||
Protos.ProvideRefund.Builder provideRefundBuilder = Protos.ProvideRefund.newBuilder()
|
||||
.setMultisigKey(ByteString.copyFrom(myKey.getPubKey()))
|
||||
.setTx(ByteString.copyFrom(((PaymentChannelV1ClientState)state).getIncompleteRefundTransaction().bitcoinSerialize()));
|
||||
|
||||
conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder()
|
||||
.setProvideRefund(provideRefundBuilder)
|
||||
.setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_REFUND)
|
||||
.build());
|
||||
conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder()
|
||||
.setProvideRefund(provideRefundBuilder)
|
||||
.setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_REFUND)
|
||||
.build());
|
||||
break;
|
||||
case 2:
|
||||
step = InitStep.WAITING_FOR_CHANNEL_OPEN;
|
||||
|
||||
// Before we can send the server the contract (ie send it to the network), we must ensure that our refund
|
||||
// transaction is safely in the wallet - thus we store it (this also keeps it up-to-date when we pay)
|
||||
state.storeChannelInWallet(serverId);
|
||||
|
||||
Protos.ProvideContract.Builder provideContractBuilder = Protos.ProvideContract.newBuilder()
|
||||
.setTx(ByteString.copyFrom(state.getContract().bitcoinSerialize()))
|
||||
.setClientKey(ByteString.copyFrom(myKey.getPubKey()));
|
||||
try {
|
||||
// Make an initial payment of the dust limit, and put it into the message as well. The size of the
|
||||
// server-requested dust limit was already sanity checked by this point.
|
||||
PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(Coin.valueOf(minPayment), userKeySetup);
|
||||
Protos.UpdatePayment.Builder initialMsg = provideContractBuilder.getInitialPaymentBuilder();
|
||||
initialMsg.setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin()));
|
||||
initialMsg.setClientChangeValue(state.getValueRefunded().value);
|
||||
} catch (ValueOutOfRangeException e) {
|
||||
throw new IllegalStateException(e); // This cannot happen.
|
||||
}
|
||||
|
||||
// Not used any more
|
||||
userKeySetup = null;
|
||||
|
||||
final Protos.TwoWayChannelMessage.Builder msg = Protos.TwoWayChannelMessage.newBuilder();
|
||||
msg.setProvideContract(provideContractBuilder);
|
||||
msg.setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_CONTRACT);
|
||||
conn.sendToServer(msg.build());
|
||||
break;
|
||||
default:
|
||||
return CloseReason.NO_ACCEPTABLE_VERSION;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void receiveRefund(Protos.TwoWayChannelMessage refundMsg, @Nullable KeyParameter userKey) throws VerificationException {
|
||||
checkState(majorVersion == 1);
|
||||
checkState(step == InitStep.WAITING_FOR_REFUND_RETURN && refundMsg.hasReturnRefund());
|
||||
log.info("Got RETURN_REFUND message, providing signed contract");
|
||||
Protos.ReturnRefund returnedRefund = refundMsg.getReturnRefund();
|
||||
state.provideRefundSignature(returnedRefund.getSignature().toByteArray(), userKey);
|
||||
// Cast is safe since we've checked the version number
|
||||
((PaymentChannelV1ClientState)state).provideRefundSignature(returnedRefund.getSignature().toByteArray(), userKey);
|
||||
step = InitStep.WAITING_FOR_CHANNEL_OPEN;
|
||||
|
||||
// Before we can send the server the contract (ie send it to the network), we must ensure that our refund
|
||||
@ -250,7 +390,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
state.storeChannelInWallet(serverId);
|
||||
|
||||
Protos.ProvideContract.Builder contractMsg = Protos.ProvideContract.newBuilder()
|
||||
.setTx(ByteString.copyFrom(state.getMultisigContract().bitcoinSerialize()));
|
||||
.setTx(ByteString.copyFrom(state.getContract().bitcoinSerialize()));
|
||||
try {
|
||||
// Make an initial payment of the dust limit, and put it into the message as well. The size of the
|
||||
// server-requested dust limit was already sanity checked by this point.
|
||||
@ -277,7 +417,16 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
if (step == InitStep.WAITING_FOR_INITIATE) {
|
||||
// We skipped the initiate step, because a previous channel that's still valid was resumed.
|
||||
wasInitiated = false;
|
||||
state = new PaymentChannelClientState(storedChannel, wallet);
|
||||
switch (majorVersion) {
|
||||
case 1:
|
||||
state = new PaymentChannelV1ClientState(storedChannel, wallet);
|
||||
break;
|
||||
case 2:
|
||||
state = new PaymentChannelV2ClientState(storedChannel, wallet);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Invalid version number " + majorVersion);
|
||||
}
|
||||
}
|
||||
step = InitStep.CHANNEL_OPEN;
|
||||
// channelOpen should disable timeouts, but
|
||||
@ -302,7 +451,8 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
checkState(step == InitStep.WAITING_FOR_VERSION_NEGOTIATION && msg.hasServerVersion());
|
||||
// Server might send back a major version lower than our own if they want to fallback to a
|
||||
// lower version. We can't handle that, so we just close the channel.
|
||||
if (msg.getServerVersion().getMajor() != SERVER_MAJOR_VERSION) {
|
||||
majorVersion = msg.getServerVersion().getMajor();
|
||||
if (!versionSelector.isServerVersionAccepted(majorVersion, msg.getServerVersion().getMinor())) {
|
||||
errorBuilder = Protos.Error.newBuilder()
|
||||
.setCode(Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION);
|
||||
closeReason = CloseReason.NO_ACCEPTABLE_VERSION;
|
||||
@ -474,8 +624,8 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
step = InitStep.WAITING_FOR_VERSION_NEGOTIATION;
|
||||
|
||||
Protos.ClientVersion.Builder versionNegotiationBuilder = Protos.ClientVersion.newBuilder()
|
||||
.setMajor(CLIENT_MAJOR_VERSION)
|
||||
.setMinor(CLIENT_MINOR_VERSION)
|
||||
.setMajor(versionSelector.getRequestedMajorVersion())
|
||||
.setMinor(versionSelector.getRequestedMinorVersion())
|
||||
.setTimeWindowSecs(timeWindow);
|
||||
|
||||
if (storedChannel != null) {
|
||||
@ -557,7 +707,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
|
||||
if (wallet.isEncrypted() && userKey == null)
|
||||
throw new ECKey.KeyIsEncryptedException();
|
||||
|
||||
PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(size, userKey);
|
||||
PaymentChannelV1ClientState.IncrementedPayment payment = state().incrementPaymentBy(size, userKey);
|
||||
Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder()
|
||||
.setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin()))
|
||||
.setClientChangeValue(state.getValueRefunded().value);
|
||||
|
@ -51,27 +51,86 @@ public class PaymentChannelClientConnection {
|
||||
* {@link org.bitcoinj.protocols.channels.PaymentChannelClient#DEFAULT_TIME_WINDOW}
|
||||
* seconds. If the server proposes a longer time the channel will be closed.
|
||||
*
|
||||
* @param server The host/port pair where the server is listening.
|
||||
* @param server The host/port pair where the server is listening.
|
||||
* @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough
|
||||
* to accommodate ECDSA signature operations and network latency.
|
||||
* @param wallet The wallet which will be paid from, and where completed transactions will be committed.
|
||||
* Must be unencrypted. Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
|
||||
* @param maxValue The maximum value this channel is allowed to request
|
||||
* @param serverId A unique ID which is used to attempt reopening of an existing channel.
|
||||
* This must be unique to the server, and, if your application is exposing payment channels to some
|
||||
* API, this should also probably encompass some caller UID to avoid applications opening channels
|
||||
* which were created by others.
|
||||
*
|
||||
* @throws IOException if there's an issue using the network.
|
||||
* @param wallet The wallet which will be paid from, and where completed transactions will be committed.
|
||||
* Must be unencrypted. Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
|
||||
* @param maxValue The maximum value this channel is allowed to request
|
||||
* @param serverId A unique ID which is used to attempt reopening of an existing channel.
|
||||
* This must be unique to the server, and, if your application is exposing payment channels to some
|
||||
* API, this should also probably encompass some caller UID to avoid applications opening channels
|
||||
* which were created by others.
|
||||
* @throws IOException if there's an issue using the network.
|
||||
* @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue.
|
||||
*/
|
||||
public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey,
|
||||
Coin maxValue, String serverId) throws IOException, ValueOutOfRangeException {
|
||||
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId,
|
||||
PaymentChannelClient.DEFAULT_TIME_WINDOW, null);
|
||||
PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the
|
||||
* connection is open. The server is requested to keep the channel open for
|
||||
* {@link org.bitcoinj.protocols.channels.PaymentChannelClient#DEFAULT_TIME_WINDOW}
|
||||
* seconds. If the server proposes a longer time the channel will be closed.
|
||||
*
|
||||
* @param server The host/port pair where the server is listening.
|
||||
* @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough
|
||||
* to accommodate ECDSA signature operations and network latency.
|
||||
* @param wallet The wallet which will be paid from, and where completed transactions will be committed.
|
||||
* Must be unencrypted. Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
|
||||
* @param maxValue The maximum value this channel is allowed to request
|
||||
* @param serverId A unique ID which is used to attempt reopening of an existing channel.
|
||||
* This must be unique to the server, and, if your application is exposing payment channels to some
|
||||
* API, this should also probably encompass some caller UID to avoid applications opening channels
|
||||
* which were created by others.
|
||||
* @param versionSelector An enum indicating which versions to support:
|
||||
* VERSION_1: use only version 1 of the protocol
|
||||
* VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1
|
||||
* VERSION_2: suggest version 2 and enforce use of version 2
|
||||
* @throws IOException if there's an issue using the network.
|
||||
* @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue.
|
||||
*/
|
||||
public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey,
|
||||
Coin maxValue, String serverId, PaymentChannelClient.VersionSelector versionSelector) throws IOException, ValueOutOfRangeException {
|
||||
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId,
|
||||
PaymentChannelClient.DEFAULT_TIME_WINDOW, null, versionSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the
|
||||
* connection is open. The server is requested to keep the channel open for {@param timeWindow}
|
||||
* seconds. If the server proposes a longer time the channel will be closed.
|
||||
*
|
||||
* @param server The host/port pair where the server is listening.
|
||||
* @param timeoutSeconds The connection timeout and read timeout during initialization. This should be large enough
|
||||
* to accommodate ECDSA signature operations and network latency.
|
||||
* @param wallet The wallet which will be paid from, and where completed transactions will be committed.
|
||||
* Can be encrypted if user key is supplied when needed. Must already have a
|
||||
* {@link StoredPaymentChannelClientStates} object in its extensions set.
|
||||
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
|
||||
* @param maxValue The maximum value this channel is allowed to request
|
||||
* @param serverId A unique ID which is used to attempt reopening of an existing channel.
|
||||
* This must be unique to the server, and, if your application is exposing payment channels to some
|
||||
* API, this should also probably encompass some caller UID to avoid applications opening channels
|
||||
* which were created by others.
|
||||
* @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open.
|
||||
* @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup.
|
||||
* @throws IOException if there's an issue using the network.
|
||||
* @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue.
|
||||
*/
|
||||
public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey,
|
||||
Coin maxValue, String serverId, final long timeWindow,
|
||||
@Nullable KeyParameter userKeySetup) throws IOException, ValueOutOfRangeException {
|
||||
this(server, timeoutSeconds, wallet, myKey, maxValue, serverId,
|
||||
timeWindow, userKeySetup, PaymentChannelClient.VersionSelector.VERSION_2_ALLOW_1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempts to open a new connection to and open a payment channel with the given host and port, blocking until the
|
||||
* connection is open. The server is requested to keep the channel open for {@param timeWindow}
|
||||
@ -91,13 +150,17 @@ public class PaymentChannelClientConnection {
|
||||
* which were created by others.
|
||||
* @param timeWindow The time in seconds, relative to now, on how long this channel should be kept open.
|
||||
* @param userKeySetup Key derived from a user password, used to decrypt myKey, if it is encrypted, during setup.
|
||||
* @param versionSelector An enum indicating which versions to support:
|
||||
* VERSION_1: use only version 1 of the protocol
|
||||
* VERSION_2_ALLOW_1: suggest version 2 but allow downgrade to version 1
|
||||
* VERSION_2: suggest version 2 and enforce use of version 2
|
||||
*
|
||||
* @throws IOException if there's an issue using the network.
|
||||
* @throws ValueOutOfRangeException if the balance of wallet is lower than maxValue.
|
||||
*/
|
||||
public PaymentChannelClientConnection(InetSocketAddress server, int timeoutSeconds, Wallet wallet, ECKey myKey,
|
||||
Coin maxValue, String serverId, final long timeWindow,
|
||||
@Nullable KeyParameter userKeySetup)
|
||||
@Nullable KeyParameter userKeySetup, PaymentChannelClient.VersionSelector versionSelector)
|
||||
throws IOException, ValueOutOfRangeException {
|
||||
// Glue the object which vends/ingests protobuf messages in order to manage state to the network object which
|
||||
// reads/writes them to the wire in length prefixed form.
|
||||
@ -125,7 +188,7 @@ public class PaymentChannelClientConnection {
|
||||
// Inform the API user that we're done and ready to roll.
|
||||
channelOpenFuture.set(PaymentChannelClientConnection.this);
|
||||
}
|
||||
});
|
||||
}, versionSelector);
|
||||
|
||||
// And glue back in the opposite direction - network to the channelClient.
|
||||
wireParser = new ProtobufConnection<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>
|
||||
*
|
||||
* <p>Note that if you call any methods which update state directly the server will not be notified and channel
|
||||
|
@ -16,24 +16,22 @@
|
||||
|
||||
package org.bitcoinj.protocols.channels;
|
||||
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
import org.bitcoinj.wallet.AllowUnconfirmedCoinSelector;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.core.listeners.AbstractWalletEventListener;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.base.Preconditions.*;
|
||||
import org.bitcoinj.core.listeners.WalletCoinsReceivedEventListener;
|
||||
@ -44,6 +42,9 @@ import org.bitcoinj.core.listeners.WalletCoinsReceivedEventListener;
|
||||
* implement micropayments or other payment schemes in which immediate settlement is not required, but zero trust
|
||||
* negotiation is. Note that this class only allows the amount of money sent to be incremented, not decremented.</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
|
||||
* by {@link PaymentChannelServerState} and {@link PaymentChannelClientConnection} implements a network protocol
|
||||
* suitable for TCP/IP connections which moves this class through each state. We say that the party who is sending funds
|
||||
@ -58,38 +59,20 @@ import org.bitcoinj.core.listeners.WalletCoinsReceivedEventListener;
|
||||
* the given time (within a few hours), the channel must be closed or else the client will broadcast the refund
|
||||
* transaction and take back all the money once the expiry time is reached.</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
|
||||
* exception will be thrown at this point. Once this is done, call
|
||||
* {@link PaymentChannelClientState#getIncompleteRefundTransaction()} and pass the resultant transaction through to the
|
||||
* server. Once you have retrieved the signature, use {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)}.
|
||||
* You must then call {@link PaymentChannelClientState#storeChannelInWallet(Sha256Hash)} to store the refund transaction
|
||||
* {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction()} and pass the resultant transaction through to the
|
||||
* server. Once you have retrieved the signature, use {@link PaymentChannelV1ClientState#provideRefundSignature(byte[], KeyParameter)}.
|
||||
* You must then call {@link PaymentChannelV1ClientState#storeChannelInWallet(Sha256Hash)} to store the refund transaction
|
||||
* in the wallet, protecting you against a malicious server attempting to destroy all your coins. At this point, you can
|
||||
* provide the server with the multi-sig contract (via {@link PaymentChannelClientState#getMultisigContract()}) safely.
|
||||
* provide the server with the multi-sig contract (via {@link PaymentChannelV1ClientState#getContract()}) safely.
|
||||
* </p>
|
||||
*/
|
||||
public class PaymentChannelClientState {
|
||||
public abstract class PaymentChannelClientState {
|
||||
private static final Logger log = LoggerFactory.getLogger(PaymentChannelClientState.class);
|
||||
|
||||
private final Wallet wallet;
|
||||
// Both sides need a key (private in our case, public for the server) in order to manage the multisig contract
|
||||
// and transactions that spend it.
|
||||
private final ECKey myKey, serverMultisigKey;
|
||||
// How much value (in satoshis) is locked up into the channel.
|
||||
private final Coin totalValue;
|
||||
// When the channel will automatically settle in favor of the client, if the server halts before protocol termination
|
||||
// specified in terms of block timestamps (so it can off real time by a few hours).
|
||||
private final long expiryTime;
|
||||
|
||||
// The refund is a time locked transaction that spends all the money of the channel back to the client.
|
||||
private Transaction refundTx;
|
||||
private Coin refundFees;
|
||||
// The multi-sig contract locks the value of the channel up such that the agreement of both parties is required
|
||||
// to spend it.
|
||||
private Transaction multisigContract;
|
||||
private Script multisigScript;
|
||||
// How much value is currently allocated to us. Starts as being same as totalValue.
|
||||
private Coin valueToMe;
|
||||
protected Coin valueToMe;
|
||||
|
||||
/**
|
||||
* The different logical states the channel can be in. The channel starts out as NEW, and then steps through the
|
||||
@ -97,6 +80,7 @@ public class PaymentChannelClientState {
|
||||
* by the time the NEW state is reached.
|
||||
*/
|
||||
public enum State {
|
||||
UNINITIALISED,
|
||||
NEW,
|
||||
INITIATED,
|
||||
WAITING_FOR_SIGNED_REFUND,
|
||||
@ -106,26 +90,24 @@ public class PaymentChannelClientState {
|
||||
EXPIRED,
|
||||
CLOSED
|
||||
}
|
||||
private State state;
|
||||
protected final StateMachine<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
|
||||
private StoredClientChannel storedChannel;
|
||||
protected StoredClientChannel storedChannel;
|
||||
|
||||
PaymentChannelClientState(StoredClientChannel storedClientChannel, Wallet wallet) throws VerificationException {
|
||||
// The PaymentChannelClientConnection handles storedClientChannel.active and ensures we aren't resuming channels
|
||||
this.stateMachine = new StateMachine<State>(State.UNINITIALISED, getStateTransitions());
|
||||
this.wallet = checkNotNull(wallet);
|
||||
this.multisigContract = checkNotNull(storedClientChannel.contract);
|
||||
this.multisigScript = multisigContract.getOutput(0).getScriptPubKey();
|
||||
this.refundTx = checkNotNull(storedClientChannel.refund);
|
||||
this.refundFees = checkNotNull(storedClientChannel.refundFees);
|
||||
this.expiryTime = refundTx.getLockTime();
|
||||
this.myKey = checkNotNull(storedClientChannel.myKey);
|
||||
this.serverMultisigKey = null;
|
||||
this.totalValue = multisigContract.getOutput(0).getValue();
|
||||
this.valueToMe = checkNotNull(storedClientChannel.valueToMe);
|
||||
this.serverKey = checkNotNull(storedClientChannel.serverKey);
|
||||
this.storedChannel = storedClientChannel;
|
||||
this.state = State.READY;
|
||||
initWalletListeners();
|
||||
this.valueToMe = checkNotNull(storedClientChannel.valueToMe);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -134,7 +116,7 @@ public class PaymentChannelClientState {
|
||||
public synchronized boolean isSettlementTransaction(Transaction tx) {
|
||||
try {
|
||||
tx.verify();
|
||||
tx.getInput(0).verify(multisigContract.getOutput(0));
|
||||
tx.getInput(0).verify(getContractInternal().getOutput(0));
|
||||
return true;
|
||||
} catch (VerificationException e) {
|
||||
return false;
|
||||
@ -143,32 +125,29 @@ public class PaymentChannelClientState {
|
||||
|
||||
/**
|
||||
* Creates a state object for a payment channel client. It is expected that you be ready to
|
||||
* {@link PaymentChannelClientState#initiate()} after construction (to avoid creating objects for channels which are
|
||||
* {@link PaymentChannelV1ClientState#initiate()} after construction (to avoid creating objects for channels which are
|
||||
* not going to finish opening) and thus some parameters provided here are only used in
|
||||
* {@link PaymentChannelClientState#initiate()} to create the Multisig contract and refund transaction.
|
||||
* {@link PaymentChannelV1ClientState#initiate()} to create the Multisig contract and refund transaction.
|
||||
*
|
||||
* @param wallet a wallet that contains at least the specified amount of value.
|
||||
* @param myKey a freshly generated private key for this channel.
|
||||
* @param serverMultisigKey a public key retrieved from the server used for the initial multisig contract
|
||||
* @param serverKey a public key retrieved from the server used for the initial multisig contract
|
||||
* @param value how many satoshis to put into this contract. If the channel reaches this limit, it must be closed.
|
||||
* It is suggested you use at least {@link Coin#CENT} to avoid paying fees if you need to spend the refund transaction
|
||||
* @param expiryTimeInSeconds At what point (UNIX timestamp +/- a few hours) the channel will expire
|
||||
*
|
||||
* @throws VerificationException If either myKey's pubkey or serverMultisigKey's pubkey are non-canonical (ie invalid)
|
||||
* @throws VerificationException If either myKey's pubkey or serverKey's pubkey are non-canonical (ie invalid)
|
||||
*/
|
||||
public PaymentChannelClientState(Wallet wallet, ECKey myKey, ECKey serverMultisigKey,
|
||||
public PaymentChannelClientState(Wallet wallet, ECKey myKey, ECKey serverKey,
|
||||
Coin value, long expiryTimeInSeconds) throws VerificationException {
|
||||
checkArgument(value.signum() > 0);
|
||||
this.stateMachine = new StateMachine<State>(State.UNINITIALISED, getStateTransitions());
|
||||
this.wallet = checkNotNull(wallet);
|
||||
initWalletListeners();
|
||||
this.serverMultisigKey = checkNotNull(serverMultisigKey);
|
||||
this.serverKey = checkNotNull(serverKey);
|
||||
this.myKey = checkNotNull(myKey);
|
||||
this.valueToMe = this.totalValue = checkNotNull(value);
|
||||
this.expiryTime = expiryTimeInSeconds;
|
||||
this.state = State.NEW;
|
||||
this.valueToMe = checkNotNull(value);
|
||||
}
|
||||
|
||||
private synchronized void initWalletListeners() {
|
||||
protected synchronized void initWalletListeners() {
|
||||
// Register a listener that watches out for the server closing the channel.
|
||||
if (storedChannel != null && storedChannel.close != null) {
|
||||
watchCloseConfirmations();
|
||||
@ -177,11 +156,11 @@ public class PaymentChannelClientState {
|
||||
@Override
|
||||
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
|
||||
synchronized (PaymentChannelClientState.this) {
|
||||
if (multisigContract == null) return;
|
||||
if (getContractInternal() == null) return;
|
||||
if (isSettlementTransaction(tx)) {
|
||||
log.info("Close: transaction {} closed contract {}", tx.getHash(), multisigContract.getHash());
|
||||
log.info("Close: transaction {} closed contract {}", tx.getHash(), getContractInternal().getHash());
|
||||
// Record the fact that it was closed along with the transaction that closed it.
|
||||
state = State.CLOSED;
|
||||
stateMachine.transition(State.CLOSED);
|
||||
if (storedChannel == null) return;
|
||||
storedChannel.close = tx;
|
||||
updateChannelInWallet();
|
||||
@ -192,7 +171,7 @@ public class PaymentChannelClientState {
|
||||
});
|
||||
}
|
||||
|
||||
private void watchCloseConfirmations() {
|
||||
protected void watchCloseConfirmations() {
|
||||
// When we see the close transaction get enough confirmations, we can just delete the record
|
||||
// of this channel along with the refund tx from the wallet, because we're not going to need
|
||||
// any of that any more.
|
||||
@ -220,18 +199,19 @@ public class PaymentChannelClientState {
|
||||
storedChannel = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This object implements a state machine, and this accessor returns which state it's currently in.
|
||||
*/
|
||||
public synchronized State getState() {
|
||||
return state;
|
||||
return stateMachine.getState();
|
||||
}
|
||||
|
||||
protected abstract Multimap<State, State> getStateTransitions();
|
||||
|
||||
public abstract int getMajorVersion();
|
||||
|
||||
/**
|
||||
* Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate
|
||||
* time using {@link PaymentChannelClientState#getIncompleteRefundTransaction} and
|
||||
* {@link PaymentChannelClientState#getMultisigContract()}. The way the contract is crafted can be adjusted by
|
||||
* overriding {@link PaymentChannelClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}.
|
||||
* time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and
|
||||
* {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by
|
||||
* overriding {@link PaymentChannelV1ClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}.
|
||||
* By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low.
|
||||
*
|
||||
* @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network
|
||||
@ -243,9 +223,9 @@ public class PaymentChannelClientState {
|
||||
|
||||
/**
|
||||
* Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate
|
||||
* time using {@link PaymentChannelClientState#getIncompleteRefundTransaction} and
|
||||
* {@link PaymentChannelClientState#getMultisigContract()}. The way the contract is crafted can be adjusted by
|
||||
* overriding {@link PaymentChannelClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}.
|
||||
* time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and
|
||||
* {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by
|
||||
* overriding {@link PaymentChannelV1ClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}.
|
||||
* By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low.
|
||||
* @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted.
|
||||
* The wallet KeyCrypter is assumed.
|
||||
@ -253,52 +233,7 @@ public class PaymentChannelClientState {
|
||||
* @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network
|
||||
* @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate
|
||||
*/
|
||||
public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException {
|
||||
final NetworkParameters params = wallet.getParams();
|
||||
Transaction template = new Transaction(params);
|
||||
// We always place the client key before the server key because, if either side wants some privacy, they can
|
||||
// use a fresh key for the the multisig contract and nowhere else
|
||||
List<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.
|
||||
}
|
||||
public abstract void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException;
|
||||
|
||||
/**
|
||||
* You can override this method in order to control the construction of the initial contract that creates the
|
||||
@ -309,68 +244,13 @@ public class PaymentChannelClientState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transaction that locks the money to the agreement of both parties. Do not mutate the result.
|
||||
* Once this step is done, you can use {@link PaymentChannelClientState#incrementPaymentBy(Coin, KeyParameter)} to
|
||||
* start paying the server.
|
||||
* Gets the contract which was used to initialize this channel
|
||||
*/
|
||||
public synchronized Transaction getMultisigContract() {
|
||||
checkState(multisigContract != null);
|
||||
if (state == State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER)
|
||||
state = State.READY;
|
||||
return multisigContract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a partially signed (invalid) refund transaction that should be passed to the server. Once the server
|
||||
* has checked it out and provided its own signature, call
|
||||
* {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)} with the result.
|
||||
*/
|
||||
public synchronized Transaction getIncompleteRefundTransaction() {
|
||||
checkState(refundTx != null);
|
||||
if (state == State.INITIATED)
|
||||
state = State.WAITING_FOR_SIGNED_REFUND;
|
||||
return refundTx;
|
||||
}
|
||||
|
||||
/**
|
||||
* <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;
|
||||
}
|
||||
public abstract Transaction getContract();
|
||||
|
||||
private synchronized Transaction makeUnsignedChannelContract(Coin valueToMe) throws ValueOutOfRangeException {
|
||||
Transaction tx = new Transaction(wallet.getParams());
|
||||
tx.addInput(multisigContract.getOutput(0));
|
||||
tx.addInput(getContractInternal().getOutput(0));
|
||||
// Our output always comes first.
|
||||
// TODO: We should drop myKey in favor of output key + multisig key separation
|
||||
// (as its always obvious who the client is based on T2 output order)
|
||||
@ -383,8 +263,8 @@ public class PaymentChannelClientState {
|
||||
* storage and throwing an {@link IllegalStateException} if it is.
|
||||
*/
|
||||
public synchronized void checkNotExpired() {
|
||||
if (Utils.currentTimeSeconds() > expiryTime) {
|
||||
state = State.EXPIRED;
|
||||
if (Utils.currentTimeSeconds() > getExpiryTime()) {
|
||||
stateMachine.transition(State.EXPIRED);
|
||||
disconnectFromChannel();
|
||||
throw new IllegalStateException("Channel expired");
|
||||
}
|
||||
@ -404,8 +284,8 @@ public class PaymentChannelClientState {
|
||||
* <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>
|
||||
*
|
||||
* <p>To spend the whole channel increment by {@link PaymentChannelClientState#getTotalValue()} -
|
||||
* {@link PaymentChannelClientState#getValueRefunded()}</p>
|
||||
* <p>To spend the whole channel increment by {@link PaymentChannelV1ClientState#getTotalValue()} -
|
||||
* {@link PaymentChannelV1ClientState#getValueRefunded()}</p>
|
||||
*
|
||||
* @param size How many satoshis to increment the payment by (note: not the new total).
|
||||
* @throws ValueOutOfRangeException If size is negative or the channel does not have sufficient money in it to
|
||||
@ -413,15 +293,15 @@ public class PaymentChannelClientState {
|
||||
*/
|
||||
public synchronized IncrementedPayment incrementPaymentBy(Coin size, @Nullable KeyParameter userKey)
|
||||
throws ValueOutOfRangeException {
|
||||
checkState(state == State.READY);
|
||||
stateMachine.checkState(State.READY);
|
||||
checkNotExpired();
|
||||
checkNotNull(size); // Validity of size will be checked by makeUnsignedChannelContract.
|
||||
if (size.signum() < 0)
|
||||
throw new ValueOutOfRangeException("Tried to decrement payment");
|
||||
Coin newValueToMe = valueToMe.subtract(size);
|
||||
Coin newValueToMe = getValueToMe().subtract(size);
|
||||
if (newValueToMe.compareTo(Transaction.MIN_NONDUST_OUTPUT) < 0 && newValueToMe.signum() > 0) {
|
||||
log.info("New value being sent back as change was smaller than minimum nondust output, sending all");
|
||||
size = valueToMe;
|
||||
size = getValueToMe();
|
||||
newValueToMe = Coin.ZERO;
|
||||
}
|
||||
if (newValueToMe.signum() < 0)
|
||||
@ -435,7 +315,7 @@ public class PaymentChannelClientState {
|
||||
mode = Transaction.SigHash.NONE;
|
||||
else
|
||||
mode = Transaction.SigHash.SINGLE;
|
||||
TransactionSignature sig = tx.calculateSignature(0, myKey.maybeDecrypt(userKey), multisigScript, mode, true);
|
||||
TransactionSignature sig = tx.calculateSignature(0, myKey.maybeDecrypt(userKey), getSignedScript(), mode, true);
|
||||
valueToMe = newValueToMe;
|
||||
updateChannelInWallet();
|
||||
IncrementedPayment payment = new IncrementedPayment();
|
||||
@ -444,10 +324,10 @@ public class PaymentChannelClientState {
|
||||
return payment;
|
||||
}
|
||||
|
||||
private synchronized void updateChannelInWallet() {
|
||||
protected synchronized void updateChannelInWallet() {
|
||||
if (storedChannel == null)
|
||||
return;
|
||||
storedChannel.valueToMe = valueToMe;
|
||||
storedChannel.valueToMe = getValueToMe();
|
||||
StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates)
|
||||
wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
|
||||
channels.updatedChannel(storedChannel);
|
||||
@ -457,7 +337,7 @@ public class PaymentChannelClientState {
|
||||
* Sets this channel's state in {@link StoredPaymentChannelClientStates} to unopened so this channel can be reopened
|
||||
* later.
|
||||
*
|
||||
* @see PaymentChannelClientState#storeChannelInWallet(Sha256Hash)
|
||||
* @see PaymentChannelV1ClientState#storeChannelInWallet(Sha256Hash)
|
||||
*/
|
||||
public synchronized void disconnectFromChannel() {
|
||||
if (storedChannel == null)
|
||||
@ -472,21 +352,14 @@ public class PaymentChannelClientState {
|
||||
*/
|
||||
@VisibleForTesting synchronized void fakeSave() {
|
||||
try {
|
||||
wallet.commitTx(multisigContract);
|
||||
wallet.commitTx(getContractInternal());
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e); // We created it
|
||||
}
|
||||
state = State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER;
|
||||
stateMachine.transition(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER);
|
||||
}
|
||||
|
||||
@VisibleForTesting synchronized void doStoreChannelInWallet(Sha256Hash id) {
|
||||
StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates)
|
||||
wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
|
||||
checkNotNull(channels, "You have not added the StoredPaymentChannelClientStates extension to the wallet.");
|
||||
checkState(channels.getChannel(id, multisigContract.getHash()) == null);
|
||||
storedChannel = new StoredClientChannel(id, multisigContract, refundTx, myKey, valueToMe, refundFees, true);
|
||||
channels.putChannel(storedChannel);
|
||||
}
|
||||
@VisibleForTesting abstract void doStoreChannelInWallet(Sha256Hash id);
|
||||
|
||||
/**
|
||||
* <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.
|
||||
*/
|
||||
public synchronized void storeChannelInWallet(Sha256Hash id) {
|
||||
checkState(state == State.SAVE_STATE_IN_WALLET && id != null);
|
||||
stateMachine.checkState(State.SAVE_STATE_IN_WALLET);
|
||||
checkState(id != null);
|
||||
if (storedChannel != null) {
|
||||
checkState(storedChannel.id.equals(id));
|
||||
return;
|
||||
@ -509,44 +383,31 @@ public class PaymentChannelClientState {
|
||||
doStoreChannelInWallet(id);
|
||||
|
||||
try {
|
||||
wallet.commitTx(multisigContract);
|
||||
wallet.commitTx(getContractInternal());
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e); // We created it
|
||||
}
|
||||
state = State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER;
|
||||
stateMachine.transition(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fees that will be paid if the refund transaction has to be claimed because the server failed to settle
|
||||
* the channel properly. May only be called after {@link PaymentChannelClientState#initiate()}
|
||||
* the channel properly. May only be called after {@link PaymentChannelV1ClientState#initiate()}
|
||||
*/
|
||||
public synchronized Coin getRefundTxFees() {
|
||||
checkState(state.compareTo(State.NEW) > 0);
|
||||
return refundFees;
|
||||
}
|
||||
public abstract Coin getRefundTxFees();
|
||||
|
||||
/**
|
||||
* Once the servers signature over the refund transaction has been received and provided using
|
||||
* {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)} then this
|
||||
* method can be called to receive the now valid and broadcastable refund transaction.
|
||||
*/
|
||||
public synchronized Transaction getCompletedRefundTransaction() {
|
||||
checkState(state.compareTo(State.WAITING_FOR_SIGNED_REFUND) > 0);
|
||||
return refundTx;
|
||||
}
|
||||
@VisibleForTesting abstract Transaction getRefundTransaction();
|
||||
|
||||
/**
|
||||
* Gets the total value of this channel (ie the maximum payment possible)
|
||||
*/
|
||||
public Coin getTotalValue() {
|
||||
return totalValue;
|
||||
}
|
||||
public abstract Coin getTotalValue();
|
||||
|
||||
/**
|
||||
* Gets the current amount refunded to us from the multisig contract (ie totalValue-valueSentToServer)
|
||||
*/
|
||||
public synchronized Coin getValueRefunded() {
|
||||
checkState(state == State.READY);
|
||||
stateMachine.checkState(State.READY);
|
||||
return valueToMe;
|
||||
}
|
||||
|
||||
@ -556,4 +417,23 @@ public class PaymentChannelClientState {
|
||||
public synchronized Coin getValueSpent() {
|
||||
return getTotalValue().subtract(getValueRefunded());
|
||||
}
|
||||
|
||||
protected abstract Coin getValueToMe();
|
||||
|
||||
protected abstract long getExpiryTime();
|
||||
|
||||
/**
|
||||
* Gets the contract without changing the state machine
|
||||
* @return
|
||||
*/
|
||||
protected abstract Transaction getContractInternal();
|
||||
|
||||
protected abstract Script getContractScript();
|
||||
|
||||
/**
|
||||
* Gets the script that is signed. In the case of a P2SH contract this is the
|
||||
* script inside the P2SH script.
|
||||
* @return
|
||||
*/
|
||||
protected abstract Script getSignedScript();
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package org.bitcoinj.protocols.channels;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
@ -28,6 +29,7 @@ import org.bitcoin.paymentchannel.Protos;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
@ -47,12 +49,17 @@ public class PaymentChannelServer {
|
||||
private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelServer.class);
|
||||
|
||||
protected final ReentrantLock lock = Threading.lock("channelserver");
|
||||
public final int SERVER_MAJOR_VERSION = 1;
|
||||
public final int SERVER_MINOR_VERSION = 0;
|
||||
|
||||
/**
|
||||
* A map of supported versions; keys are major versions, and the corresponding
|
||||
* value is the minor version at that major level.
|
||||
*/
|
||||
public static final Map<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
|
||||
private enum InitStep {
|
||||
WAITING_ON_CLIENT_VERSION,
|
||||
// This step is only used in V1 of the protocol.
|
||||
WAITING_ON_UNSIGNED_REFUND,
|
||||
WAITING_ON_CONTRACT,
|
||||
WAITING_ON_MULTISIG_ACCEPTANCE,
|
||||
@ -111,6 +118,9 @@ public class PaymentChannelServer {
|
||||
}
|
||||
private final ServerConnection conn;
|
||||
|
||||
// Used to track the negotiated version number
|
||||
@GuardedBy("lock") private int majorVersion;
|
||||
|
||||
// Used to keep track of whether or not the "socket" ie connection is open and we can generate messages
|
||||
@GuardedBy("lock") private boolean connectionOpen = false;
|
||||
// Indicates that no further messages should be sent and we intend to settle the connection
|
||||
@ -211,15 +221,15 @@ public class PaymentChannelServer {
|
||||
private void receiveVersionMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
|
||||
checkState(step == InitStep.WAITING_ON_CLIENT_VERSION && msg.hasClientVersion());
|
||||
final Protos.ClientVersion clientVersion = msg.getClientVersion();
|
||||
final int major = clientVersion.getMajor();
|
||||
if (major != SERVER_MAJOR_VERSION) {
|
||||
error("This server needs protocol version " + SERVER_MAJOR_VERSION + " , client offered " + major,
|
||||
majorVersion = clientVersion.getMajor();
|
||||
if (!SERVER_VERSIONS.containsKey(majorVersion)) {
|
||||
error("This server needs one of protocol versions " + SERVER_VERSIONS.keySet() + " , client offered " + majorVersion,
|
||||
Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION, CloseReason.NO_ACCEPTABLE_VERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
Protos.ServerVersion.Builder versionNegotiationBuilder = Protos.ServerVersion.newBuilder()
|
||||
.setMajor(SERVER_MAJOR_VERSION).setMinor(SERVER_MINOR_VERSION);
|
||||
.setMajor(majorVersion).setMinor(SERVER_VERSIONS.get(majorVersion));
|
||||
conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder()
|
||||
.setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION)
|
||||
.setServerVersion(versionNegotiationBuilder)
|
||||
@ -262,7 +272,17 @@ public class PaymentChannelServer {
|
||||
wallet.freshReceiveKey();
|
||||
|
||||
expireTime = Utils.currentTimeSeconds() + truncateTimeWindow(clientVersion.getTimeWindowSecs());
|
||||
step = InitStep.WAITING_ON_UNSIGNED_REFUND;
|
||||
switch (majorVersion) {
|
||||
case 1:
|
||||
step = InitStep.WAITING_ON_UNSIGNED_REFUND;
|
||||
break;
|
||||
case 2:
|
||||
step = InitStep.WAITING_ON_CONTRACT;
|
||||
break;
|
||||
default:
|
||||
error("Protocol version " + majorVersion + " not supported", Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION, CloseReason.NO_ACCEPTABLE_VERSION);
|
||||
break;
|
||||
}
|
||||
|
||||
Protos.Initiate.Builder initiateBuilder = Protos.Initiate.newBuilder()
|
||||
.setMultisigKey(ByteString.copyFrom(myKey.getPubKey()))
|
||||
@ -290,13 +310,16 @@ public class PaymentChannelServer {
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void receiveRefundMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
|
||||
checkState(majorVersion == 1);
|
||||
checkState(step == InitStep.WAITING_ON_UNSIGNED_REFUND && msg.hasProvideRefund());
|
||||
log.info("Got refund transaction, returning signature");
|
||||
|
||||
Protos.ProvideRefund providedRefund = msg.getProvideRefund();
|
||||
state = new PaymentChannelServerState(broadcaster, wallet, myKey, expireTime);
|
||||
byte[] signature = state.provideRefundTransaction(wallet.getParams().getDefaultSerializer().makeTransaction(providedRefund.getTx().toByteArray()),
|
||||
providedRefund.getMultisigKey().toByteArray());
|
||||
state = new PaymentChannelV1ServerState(broadcaster, wallet, myKey, expireTime);
|
||||
// We can cast to V1 state since this state is only used in the V1 protocol
|
||||
byte[] signature = ((PaymentChannelV1ServerState) state)
|
||||
.provideRefundTransaction(wallet.getParams().getDefaultSerializer().makeTransaction(providedRefund.getTx().toByteArray()),
|
||||
providedRefund.getMultisigKey().toByteArray());
|
||||
|
||||
step = InitStep.WAITING_ON_CONTRACT;
|
||||
|
||||
@ -344,18 +367,25 @@ public class PaymentChannelServer {
|
||||
|
||||
@GuardedBy("lock")
|
||||
private void receiveContractMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
|
||||
checkState(majorVersion == 1 || majorVersion == 2);
|
||||
checkState(step == InitStep.WAITING_ON_CONTRACT && msg.hasProvideContract());
|
||||
log.info("Got contract, broadcasting and responding with CHANNEL_OPEN");
|
||||
final Protos.ProvideContract providedContract = msg.getProvideContract();
|
||||
|
||||
if (majorVersion == 2) {
|
||||
state = new PaymentChannelV2ServerState(broadcaster, wallet, myKey, expireTime);
|
||||
checkState(providedContract.hasClientKey(), "ProvideContract didn't have a client key in protocol v2");
|
||||
((PaymentChannelV2ServerState)state).provideClientKey(providedContract.getClientKey().toByteArray());
|
||||
}
|
||||
|
||||
//TODO notify connection handler that timeout should be significantly extended as we wait for network propagation?
|
||||
final Transaction multisigContract = wallet.getParams().getDefaultSerializer().makeTransaction(providedContract.getTx().toByteArray());
|
||||
final Transaction contract = wallet.getParams().getDefaultSerializer().makeTransaction(providedContract.getTx().toByteArray());
|
||||
step = InitStep.WAITING_ON_MULTISIG_ACCEPTANCE;
|
||||
state.provideMultiSigContract(multisigContract)
|
||||
state.provideContract(contract)
|
||||
.addListener(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
multisigContractPropogated(providedContract, multisigContract.getHash());
|
||||
multisigContractPropogated(providedContract, contract.getHash());
|
||||
}
|
||||
}, Threading.SAME_THREAD);
|
||||
}
|
||||
@ -413,9 +443,6 @@ public class PaymentChannelServer {
|
||||
checkState(connectionOpen);
|
||||
if (channelSettling)
|
||||
return;
|
||||
// If we generate an error, we set errorBuilder and closeReason and break, otherwise we return
|
||||
Protos.Error.Builder errorBuilder;
|
||||
CloseReason closeReason;
|
||||
try {
|
||||
switch (msg.getType()) {
|
||||
case CLIENT_VERSION:
|
||||
@ -532,18 +559,18 @@ public class PaymentChannelServer {
|
||||
connectionOpen = false;
|
||||
|
||||
try {
|
||||
if (state != null && state.getMultisigContract() != null) {
|
||||
if (state != null && state.getContract() != null) {
|
||||
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)
|
||||
wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID);
|
||||
if (channels != null) {
|
||||
StoredServerChannel storedServerChannel = channels.getChannel(state.getMultisigContract().getHash());
|
||||
StoredServerChannel storedServerChannel = channels.getChannel(state.getContract().getHash());
|
||||
if (storedServerChannel != null) {
|
||||
storedServerChannel.clearConnectedHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
// Expected when we call getMultisigContract() sometimes
|
||||
// Expected when we call getContract() sometimes
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
|
@ -16,24 +16,27 @@
|
||||
|
||||
package org.bitcoinj.protocols.channels;
|
||||
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import static com.google.common.base.Preconditions.*;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
/**
|
||||
* <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
|
||||
* 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
|
||||
* by {@link PaymentChannelClientState} and {@link PaymentChannelServerListener} implements the server-side network
|
||||
* by {@link PaymentChannelV1ClientState} and {@link PaymentChannelServerListener} implements the server-side network
|
||||
* protocol listening for TCP/IP connections and moving this class through each state. We say that the party who is
|
||||
* sending funds is the <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
|
||||
@ -64,7 +70,7 @@ import static com.google.common.base.Preconditions.*;
|
||||
* can then begin paying by providing us with signatures for the multi-sig contract which pay some amount back to the
|
||||
* client, and the rest is ours to do with as we wish.</p>
|
||||
*/
|
||||
public class PaymentChannelServerState {
|
||||
public abstract class PaymentChannelServerState {
|
||||
private static final Logger log = LoggerFactory.getLogger(PaymentChannelServerState.class);
|
||||
|
||||
/**
|
||||
@ -73,6 +79,7 @@ public class PaymentChannelServerState {
|
||||
* until READY, at which time the client can increase payment incrementally.
|
||||
*/
|
||||
public enum State {
|
||||
UNINITIALISED,
|
||||
WAITING_FOR_REFUND_TRANSACTION,
|
||||
WAITING_FOR_MULTISIG_CONTRACT,
|
||||
WAITING_FOR_MULTISIG_ACCEPTANCE,
|
||||
@ -81,55 +88,43 @@ public class PaymentChannelServerState {
|
||||
CLOSED,
|
||||
ERROR,
|
||||
}
|
||||
private State state;
|
||||
|
||||
// The client and server keys for the multi-sig contract
|
||||
// We currently also use the serverKey for payouts, but this is not required
|
||||
private ECKey clientKey, serverKey;
|
||||
protected StateMachine<State> stateMachine;
|
||||
|
||||
// Package-local for checkArguments in StoredServerChannel
|
||||
final Wallet wallet;
|
||||
|
||||
// The object that will broadcast transactions for us - usually a peer group.
|
||||
private final TransactionBroadcaster broadcaster;
|
||||
|
||||
// The multi-sig contract and the output script from it
|
||||
private Transaction multisigContract = null;
|
||||
private Script multisigScript;
|
||||
protected final TransactionBroadcaster broadcaster;
|
||||
|
||||
// The last signature the client provided for a payment transaction.
|
||||
private byte[] bestValueSignature;
|
||||
protected byte[] bestValueSignature;
|
||||
|
||||
// The total value locked into the multi-sig output and the value to us in the last signature the client provided
|
||||
private Coin totalValue;
|
||||
private Coin bestValueToMe = Coin.ZERO;
|
||||
private Coin feePaidForPayment;
|
||||
protected Coin bestValueToMe = Coin.ZERO;
|
||||
|
||||
// The refund/change transaction output that goes back to the client
|
||||
private TransactionOutput clientOutput;
|
||||
private long refundTransactionUnlockTimeSecs;
|
||||
// The server key for the multi-sig contract
|
||||
// We currently also use the serverKey for payouts, but this is not required
|
||||
protected ECKey serverKey;
|
||||
|
||||
private long minExpireTime;
|
||||
protected long minExpireTime;
|
||||
|
||||
private StoredServerChannel storedServerChannel = null;
|
||||
protected StoredServerChannel storedServerChannel = null;
|
||||
|
||||
// The contract and the output script from it
|
||||
protected Transaction contract = null;
|
||||
|
||||
PaymentChannelServerState(StoredServerChannel storedServerChannel, Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException {
|
||||
synchronized (storedServerChannel) {
|
||||
this.stateMachine = new StateMachine<State>(State.UNINITIALISED, getStateTransitions());
|
||||
this.wallet = checkNotNull(wallet);
|
||||
this.broadcaster = checkNotNull(broadcaster);
|
||||
this.multisigContract = checkNotNull(storedServerChannel.contract);
|
||||
this.multisigScript = multisigContract.getOutput(0).getScriptPubKey();
|
||||
this.clientKey = ECKey.fromPublicOnly(multisigScript.getChunks().get(1).data);
|
||||
this.clientOutput = checkNotNull(storedServerChannel.clientOutput);
|
||||
this.refundTransactionUnlockTimeSecs = storedServerChannel.refundTransactionUnlockTimeSecs;
|
||||
this.contract = checkNotNull(storedServerChannel.contract);
|
||||
this.serverKey = checkNotNull(storedServerChannel.myKey);
|
||||
this.totalValue = multisigContract.getOutput(0).getValue();
|
||||
this.storedServerChannel = storedServerChannel;
|
||||
this.bestValueToMe = checkNotNull(storedServerChannel.bestValueToMe);
|
||||
this.bestValueSignature = storedServerChannel.bestValueSignature;
|
||||
checkArgument(bestValueToMe.equals(Coin.ZERO) || bestValueSignature != null);
|
||||
this.storedServerChannel = storedServerChannel;
|
||||
storedServerChannel.state = this;
|
||||
this.state = State.READY;
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,119 +138,75 @@ public class PaymentChannelServerState {
|
||||
* @param minExpireTime The earliest time at which the client can claim the refund transaction (UNIX timestamp of block)
|
||||
*/
|
||||
public PaymentChannelServerState(TransactionBroadcaster broadcaster, Wallet wallet, ECKey serverKey, long minExpireTime) {
|
||||
this.state = State.WAITING_FOR_REFUND_TRANSACTION;
|
||||
this.stateMachine = new StateMachine<State>(State.UNINITIALISED, getStateTransitions());
|
||||
this.serverKey = checkNotNull(serverKey);
|
||||
this.wallet = checkNotNull(wallet);
|
||||
this.broadcaster = checkNotNull(broadcaster);
|
||||
this.minExpireTime = minExpireTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* This object implements a state machine, and this accessor returns which state it's currently in.
|
||||
*/
|
||||
public abstract int getMajorVersion();
|
||||
|
||||
public synchronized State getState() {
|
||||
return state;
|
||||
return stateMachine.getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client provides the refund transaction.
|
||||
* The refund transaction must have one input from the multisig contract (that we don't have yet) and one output
|
||||
* that the client creates to themselves. This object will later be modified when we start getting paid.
|
||||
*
|
||||
* @param refundTx The refund transaction, this object will be mutated when payment is incremented.
|
||||
* @param clientMultiSigPubKey The client's pubkey which is required for the multisig output
|
||||
* @return Our signature that makes the refund transaction valid
|
||||
* @throws VerificationException If the transaction isnt valid or did not meet the requirements of a refund transaction.
|
||||
*/
|
||||
public synchronized byte[] provideRefundTransaction(Transaction refundTx, byte[] clientMultiSigPubKey) throws VerificationException {
|
||||
checkNotNull(refundTx);
|
||||
checkNotNull(clientMultiSigPubKey);
|
||||
checkState(state == State.WAITING_FOR_REFUND_TRANSACTION);
|
||||
log.info("Provided with refund transaction: {}", refundTx);
|
||||
// Do a few very basic syntax sanity checks.
|
||||
refundTx.verify();
|
||||
// Verify that the refund transaction has a single input (that we can fill to sign the multisig output).
|
||||
if (refundTx.getInputs().size() != 1)
|
||||
throw new VerificationException("Refund transaction does not have exactly one input");
|
||||
// Verify that the refund transaction has a time lock on it and a sequence number of zero.
|
||||
if (refundTx.getInput(0).getSequenceNumber() != 0)
|
||||
throw new VerificationException("Refund transaction's input's sequence number is non-0");
|
||||
if (refundTx.getLockTime() < minExpireTime)
|
||||
throw new VerificationException("Refund transaction has a lock time too soon");
|
||||
// Verify the transaction has one output (we don't care about its contents, its up to the client)
|
||||
// Note that because we sign with SIGHASH_NONE|SIGHASH_ANYOENCANPAY the client can later add more outputs and
|
||||
// inputs, but we will need only one output later to create the paying transactions
|
||||
if (refundTx.getOutputs().size() != 1)
|
||||
throw new VerificationException("Refund transaction does not have exactly one output");
|
||||
|
||||
refundTransactionUnlockTimeSecs = refundTx.getLockTime();
|
||||
|
||||
// Sign the refund tx with the scriptPubKey and return the signature. We don't have the spending transaction
|
||||
// so do the steps individually.
|
||||
clientKey = ECKey.fromPublicOnly(clientMultiSigPubKey);
|
||||
Script multisigPubKey = ScriptBuilder.createMultiSigOutputScript(2, ImmutableList.of(clientKey, serverKey));
|
||||
// We are really only signing the fact that the transaction has a proper lock time and don't care about anything
|
||||
// else, so we sign SIGHASH_NONE and SIGHASH_ANYONECANPAY.
|
||||
TransactionSignature sig = refundTx.calculateSignature(0, serverKey, multisigPubKey, Transaction.SigHash.NONE, true);
|
||||
log.info("Signed refund transaction.");
|
||||
this.clientOutput = refundTx.getOutput(0);
|
||||
state = State.WAITING_FOR_MULTISIG_CONTRACT;
|
||||
return sig.encodeToBitcoin();
|
||||
}
|
||||
protected abstract Multimap<State, State> getStateTransitions();
|
||||
|
||||
/**
|
||||
* Called when the client provides the multi-sig contract. Checks that the previously-provided refund transaction
|
||||
* spends this transaction (because we will use it as a base to create payment transactions) as well as output value
|
||||
* and form (ie it is a 2-of-2 multisig to the correct keys).
|
||||
*
|
||||
* @param multisigContract The provided multisig contract. Do not mutate this object after this call.
|
||||
* @param contract The provided multisig contract. Do not mutate this object after this call.
|
||||
* @return A future which completes when the provided multisig contract successfully broadcasts, or throws if the broadcast fails for some reason
|
||||
* Note that if the network simply rejects the transaction, this future will never complete, a timeout should be used.
|
||||
* @throws VerificationException If the provided multisig contract is not well-formed or does not meet previously-specified parameters
|
||||
*/
|
||||
public synchronized ListenableFuture<PaymentChannelServerState> provideMultiSigContract(final Transaction multisigContract) throws VerificationException {
|
||||
checkNotNull(multisigContract);
|
||||
checkState(state == State.WAITING_FOR_MULTISIG_CONTRACT);
|
||||
public synchronized ListenableFuture<PaymentChannelServerState> provideContract(final Transaction contract) throws VerificationException {
|
||||
checkNotNull(contract);
|
||||
stateMachine.checkState(State.WAITING_FOR_MULTISIG_CONTRACT);
|
||||
try {
|
||||
multisigContract.verify();
|
||||
this.multisigContract = multisigContract;
|
||||
this.multisigScript = multisigContract.getOutput(0).getScriptPubKey();
|
||||
contract.verify();
|
||||
this.contract = contract;
|
||||
verifyContract(contract);
|
||||
|
||||
// Check that multisigContract's first output is a 2-of-2 multisig to the correct pubkeys in the correct order
|
||||
final Script expectedScript = ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(clientKey, serverKey));
|
||||
if (!Arrays.equals(multisigScript.getProgram(), expectedScript.getProgram()))
|
||||
throw new VerificationException("Multisig contract's first output was not a standard 2-of-2 multisig to client and server in that order.");
|
||||
// Check that contract's first output is a 2-of-2 multisig to the correct pubkeys in the correct order
|
||||
final Script expectedScript = createOutputScript();
|
||||
if (!Arrays.equals(getContractScript().getProgram(), expectedScript.getProgram()))
|
||||
throw new VerificationException(getMajorVersion() == 1 ?
|
||||
"Contract's first output was not a standard 2-of-2 multisig to client and server in that order." :
|
||||
"Contract was not a P2SH script of a CLTV redeem script to client and server");
|
||||
|
||||
this.totalValue = multisigContract.getOutput(0).getValue();
|
||||
if (this.totalValue.signum() <= 0)
|
||||
if (getTotalValue().signum() <= 0)
|
||||
throw new VerificationException("Not accepting an attempt to open a contract with zero value.");
|
||||
} catch (VerificationException e) {
|
||||
// We couldn't parse the multisig transaction or its output.
|
||||
log.error("Provided multisig contract did not verify: {}", multisigContract.toString());
|
||||
log.error("Provided multisig contract did not verify: {}", contract.toString());
|
||||
throw e;
|
||||
}
|
||||
log.info("Broadcasting multisig contract: {}", multisigContract);
|
||||
wallet.addWatchedScripts(ImmutableList.of(multisigContract.getOutput(0).getScriptPubKey()));
|
||||
state = State.WAITING_FOR_MULTISIG_ACCEPTANCE;
|
||||
log.info("Broadcasting multisig contract: {}", contract);
|
||||
wallet.addWatchedScripts(ImmutableList.of(contract.getOutput(0).getScriptPubKey()));
|
||||
stateMachine.transition(State.WAITING_FOR_MULTISIG_ACCEPTANCE);
|
||||
final SettableFuture<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) {
|
||||
log.info("Successfully broadcast multisig contract {}. Channel now open.", transaction.getHashAsString());
|
||||
try {
|
||||
// Manually add the multisigContract to the wallet, overriding the isRelevant checks so we can track
|
||||
// Manually add the contract to the wallet, overriding the isRelevant checks so we can track
|
||||
// it and check for double-spends later
|
||||
wallet.receivePending(multisigContract, null, true);
|
||||
wallet.receivePending(contract, null, true);
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e); // Cannot happen, we already called multisigContract.verify()
|
||||
throw new RuntimeException(e); // Cannot happen, we already called contract.verify()
|
||||
}
|
||||
state = State.READY;
|
||||
stateMachine.transition(State.READY);
|
||||
future.set(PaymentChannelServerState.this);
|
||||
}
|
||||
|
||||
@Override public void onFailure(Throwable throwable) {
|
||||
// Couldn't broadcast the transaction for some reason.
|
||||
log.error("Broadcast multisig contract failed", throwable);
|
||||
state = State.ERROR;
|
||||
log.error("Failed to broadcast contract", throwable);
|
||||
stateMachine.transition(State.ERROR);
|
||||
future.setException(throwable);
|
||||
}
|
||||
});
|
||||
@ -263,18 +214,17 @@ public class PaymentChannelServerState {
|
||||
}
|
||||
|
||||
// Create a payment transaction with valueToMe going back to us
|
||||
private synchronized Wallet.SendRequest makeUnsignedChannelContract(Coin valueToMe) {
|
||||
protected synchronized Wallet.SendRequest makeUnsignedChannelContract(Coin valueToMe) {
|
||||
Transaction tx = new Transaction(wallet.getParams());
|
||||
if (!totalValue.subtract(valueToMe).equals(Coin.ZERO)) {
|
||||
clientOutput.setValue(totalValue.subtract(valueToMe));
|
||||
tx.addOutput(clientOutput);
|
||||
if (!getTotalValue().subtract(valueToMe).equals(Coin.ZERO)) {
|
||||
tx.addOutput(getTotalValue().subtract(valueToMe), getClientKey().toAddress(wallet.getParams()));
|
||||
}
|
||||
tx.addInput(multisigContract.getOutput(0));
|
||||
tx.addInput(contract.getOutput(0));
|
||||
return Wallet.SendRequest.forTx(tx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client provides us with a new signature and wishes to increment total payment by size.
|
||||
* Called when the client provides us with a new signature and wishes to increment total payment by size. +
|
||||
* Verifies the provided signature and only updates values if everything checks out.
|
||||
* If the new refundSize is not the lowest we have seen, it is simply ignored.
|
||||
*
|
||||
@ -284,26 +234,29 @@ public class PaymentChannelServerState {
|
||||
* @return true if there is more value left on the channel, false if it is now fully used up.
|
||||
*/
|
||||
public synchronized boolean incrementPayment(Coin refundSize, byte[] signatureBytes) throws VerificationException, ValueOutOfRangeException, InsufficientMoneyException {
|
||||
checkState(state == State.READY);
|
||||
stateMachine.checkState(State.READY);
|
||||
checkNotNull(refundSize);
|
||||
checkNotNull(signatureBytes);
|
||||
TransactionSignature signature = TransactionSignature.decodeFromBitcoin(signatureBytes, true);
|
||||
// We allow snapping to zero for the payment amount because it's treated specially later, but not less than
|
||||
// the dust level because that would prevent the transaction from being relayed/mined.
|
||||
final boolean fullyUsedUp = refundSize.equals(Coin.ZERO);
|
||||
if (refundSize.compareTo(clientOutput.getMinNonDustValue()) < 0 && !fullyUsedUp)
|
||||
throw new ValueOutOfRangeException("Attempt to refund negative value or value too small to be accepted by the network");
|
||||
Coin newValueToMe = totalValue.subtract(refundSize);
|
||||
Coin newValueToMe = getTotalValue().subtract(refundSize);
|
||||
if (newValueToMe.signum() < 0)
|
||||
throw new ValueOutOfRangeException("Attempt to refund more than the contract allows.");
|
||||
if (newValueToMe.compareTo(bestValueToMe) < 0)
|
||||
throw new ValueOutOfRangeException("Attempt to roll back payment on the channel.");
|
||||
|
||||
// Get the wallet's copy of the multisigContract (ie with confidence information), if this is null, the wallet
|
||||
Wallet.SendRequest req = makeUnsignedChannelContract(newValueToMe);
|
||||
|
||||
if (!fullyUsedUp && refundSize.compareTo(req.tx.getOutput(0).getMinNonDustValue()) < 0)
|
||||
throw new ValueOutOfRangeException("Attempt to refund negative value or value too small to be accepted by the network");
|
||||
|
||||
// Get the wallet's copy of the contract (ie with confidence information), if this is null, the wallet
|
||||
// was not connected to the peergroup when the contract was broadcast (which may cause issues down the road, and
|
||||
// disables our double-spend check next)
|
||||
Transaction walletContract = wallet.getTransaction(multisigContract.getHash());
|
||||
checkNotNull(walletContract, "Wallet did not contain multisig contract {} after state was marked READY", multisigContract.getHash());
|
||||
Transaction walletContract = wallet.getTransaction(contract.getHash());
|
||||
checkNotNull(walletContract, "Wallet did not contain multisig contract {} after state was marked READY", contract.getHash());
|
||||
|
||||
// Note that we check for DEAD state here, but this test is essentially useless in production because we will
|
||||
// miss most double-spends due to bloom filtering right now anyway. This will eventually fixed by network-wide
|
||||
@ -324,13 +277,12 @@ public class PaymentChannelServerState {
|
||||
if (signature.sigHashMode() != mode || !signature.anyoneCanPay())
|
||||
throw new VerificationException("New payment signature was not signed with the right SIGHASH flags.");
|
||||
|
||||
Wallet.SendRequest req = makeUnsignedChannelContract(newValueToMe);
|
||||
// Now check the signature is correct.
|
||||
// Note that the client must sign with SIGHASH_{SINGLE/NONE} | SIGHASH_ANYONECANPAY to allow us to add additional
|
||||
// inputs (in case we need to add significant fee, or something...) and any outputs we want to pay to.
|
||||
Sha256Hash sighash = req.tx.hashForSignature(0, multisigScript, mode, true);
|
||||
Sha256Hash sighash = req.tx.hashForSignature(0, getSignedScript(), mode, true);
|
||||
|
||||
if (!clientKey.verify(sighash, signature))
|
||||
if (!getClientKey().verify(sighash, signature))
|
||||
throw new VerificationException("Signature does not verify on tx\n" + req.tx);
|
||||
bestValueToMe = newValueToMe;
|
||||
bestValueSignature = signatureBytes;
|
||||
@ -338,103 +290,15 @@ public class PaymentChannelServerState {
|
||||
return !fullyUsedUp;
|
||||
}
|
||||
|
||||
// Signs the first input of the transaction which must spend the multisig contract.
|
||||
private void signMultisigInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) {
|
||||
TransactionSignature signature = tx.calculateSignature(0, serverKey, multisigScript, hashType, anyoneCanPay);
|
||||
byte[] mySig = signature.encodeToBitcoin();
|
||||
Script scriptSig = ScriptBuilder.createMultiSigInputScriptBytes(ImmutableList.of(bestValueSignature, mySig));
|
||||
tx.getInput(0).setScriptSig(scriptSig);
|
||||
}
|
||||
|
||||
final SettableFuture<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.
|
||||
*/
|
||||
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 (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;
|
||||
}
|
||||
public abstract ListenableFuture<Transaction> close() throws InsufficientMoneyException;
|
||||
|
||||
/**
|
||||
* Gets the highest payment to ourselves (which we will receive on settle(), not including fees)
|
||||
@ -446,29 +310,21 @@ public class PaymentChannelServerState {
|
||||
/**
|
||||
* Gets the fee paid in the final payment transaction (only available if settle() did not throw an exception)
|
||||
*/
|
||||
public synchronized Coin getFeePaid() {
|
||||
checkState(state == State.CLOSED || state == State.CLOSING);
|
||||
return feePaidForPayment;
|
||||
}
|
||||
public abstract Coin getFeePaid();
|
||||
|
||||
/**
|
||||
* Gets the multisig contract which was used to initialize this channel
|
||||
*/
|
||||
public synchronized Transaction getMultisigContract() {
|
||||
checkState(multisigContract != null);
|
||||
return multisigContract;
|
||||
public synchronized Transaction getContract() {
|
||||
checkState(contract != null);
|
||||
return contract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the client's refund transaction which they can spend to get the entire channel value back if it reaches its
|
||||
* lock time.
|
||||
*/
|
||||
public synchronized long getRefundTransactionUnlockTime() {
|
||||
checkState(state.compareTo(State.WAITING_FOR_MULTISIG_CONTRACT) > 0 && state != State.ERROR);
|
||||
return refundTransactionUnlockTimeSecs;
|
||||
public long getExpiryTime() {
|
||||
return minExpireTime;
|
||||
}
|
||||
|
||||
private synchronized void updateChannelInWallet() {
|
||||
protected synchronized void updateChannelInWallet() {
|
||||
if (storedServerChannel != null) {
|
||||
storedServerChannel.updateValueToMe(bestValueToMe, bestValueSignature);
|
||||
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)
|
||||
@ -480,7 +336,7 @@ public class PaymentChannelServerState {
|
||||
/**
|
||||
* Stores this channel's state in the wallet as a part of a {@link StoredPaymentChannelServerStates} wallet
|
||||
* extension and keeps it up-to-date each time payment is incremented. This will be automatically removed when
|
||||
* a call to {@link PaymentChannelServerState#close()} completes successfully. A channel may only be stored after it
|
||||
* a call to {@link PaymentChannelV1ServerState#close()} completes successfully. A channel may only be stored after it
|
||||
* has fully opened (ie state == State.READY).
|
||||
*
|
||||
* @param connectedHandler Optional {@link PaymentChannelServer} object that manages this object. This will
|
||||
@ -489,16 +345,47 @@ public class PaymentChannelServerState {
|
||||
* handler which can then do a TCP disconnect.
|
||||
*/
|
||||
public synchronized void storeChannelInWallet(@Nullable PaymentChannelServer connectedHandler) {
|
||||
checkState(state == State.READY);
|
||||
stateMachine.checkState(State.READY);
|
||||
if (storedServerChannel != null)
|
||||
return;
|
||||
|
||||
log.info("Storing state with contract hash {}.", multisigContract.getHash());
|
||||
log.info("Storing state with contract hash {}.", getContract().getHash());
|
||||
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)
|
||||
wallet.addOrGetExistingExtension(new StoredPaymentChannelServerStates(wallet, broadcaster));
|
||||
storedServerChannel = new StoredServerChannel(this, multisigContract, clientOutput, refundTransactionUnlockTimeSecs, serverKey, bestValueToMe, bestValueSignature);
|
||||
storedServerChannel = new StoredServerChannel(this, getMajorVersion(), getContract(), getClientOutput(), getExpiryTime(), serverKey, getClientKey(), bestValueToMe, bestValueSignature);
|
||||
if (connectedHandler != null)
|
||||
checkState(storedServerChannel.setConnectedHandler(connectedHandler, false) == connectedHandler);
|
||||
channels.putChannel(storedServerChannel);
|
||||
}
|
||||
|
||||
public abstract TransactionOutput getClientOutput();
|
||||
|
||||
public Script getContractScript() {
|
||||
if (contract == null) {
|
||||
return null;
|
||||
}
|
||||
return contract.getOutput(0).getScriptPubKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the script that signatures should sign against. This is never a P2SH
|
||||
* script, rather the script that would be inside a P2SH script.
|
||||
* @return
|
||||
*/
|
||||
protected abstract Script getSignedScript();
|
||||
|
||||
/**
|
||||
* Verifies that the given contract meets a set of extra requirements
|
||||
* @param contract
|
||||
*/
|
||||
protected void verifyContract(final Transaction contract) {
|
||||
}
|
||||
|
||||
protected abstract Script createOutputScript();
|
||||
|
||||
protected Coin getTotalValue() {
|
||||
return contract.getOutput(0).getValue();
|
||||
}
|
||||
|
||||
protected abstract ECKey getClientKey();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -745,11 +745,11 @@ public final class ServerState {
|
||||
com.google.protobuf.ByteString getContractTransaction();
|
||||
|
||||
/**
|
||||
* <code>required bytes clientOutput = 5;</code>
|
||||
* <code>optional bytes clientOutput = 5;</code>
|
||||
*/
|
||||
boolean hasClientOutput();
|
||||
/**
|
||||
* <code>required bytes clientOutput = 5;</code>
|
||||
* <code>optional bytes clientOutput = 5;</code>
|
||||
*/
|
||||
com.google.protobuf.ByteString getClientOutput();
|
||||
|
||||
@ -761,6 +761,32 @@ public final class ServerState {
|
||||
* <code>required bytes myKey = 6;</code>
|
||||
*/
|
||||
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}
|
||||
@ -848,6 +874,16 @@ public final class ServerState {
|
||||
myKey_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
case 56: {
|
||||
bitField0_ |= 0x00000040;
|
||||
majorVersion_ = input.readUInt32();
|
||||
break;
|
||||
}
|
||||
case 66: {
|
||||
bitField0_ |= 0x00000080;
|
||||
clientKey_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
@ -951,13 +987,13 @@ public final class ServerState {
|
||||
public static final int CLIENTOUTPUT_FIELD_NUMBER = 5;
|
||||
private com.google.protobuf.ByteString clientOutput_;
|
||||
/**
|
||||
* <code>required bytes clientOutput = 5;</code>
|
||||
* <code>optional bytes clientOutput = 5;</code>
|
||||
*/
|
||||
public boolean hasClientOutput() {
|
||||
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||
}
|
||||
/**
|
||||
* <code>required bytes clientOutput = 5;</code>
|
||||
* <code>optional bytes clientOutput = 5;</code>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getClientOutput() {
|
||||
return clientOutput_;
|
||||
@ -978,6 +1014,44 @@ public final class ServerState {
|
||||
return myKey_;
|
||||
}
|
||||
|
||||
public static final int MAJORVERSION_FIELD_NUMBER = 7;
|
||||
private int majorVersion_;
|
||||
/**
|
||||
* <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() {
|
||||
bestValueToMe_ = 0L;
|
||||
bestValueSignature_ = com.google.protobuf.ByteString.EMPTY;
|
||||
@ -985,6 +1059,8 @@ public final class ServerState {
|
||||
contractTransaction_ = com.google.protobuf.ByteString.EMPTY;
|
||||
clientOutput_ = com.google.protobuf.ByteString.EMPTY;
|
||||
myKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
majorVersion_ = 1;
|
||||
clientKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
}
|
||||
private byte memoizedIsInitialized = -1;
|
||||
public final boolean isInitialized() {
|
||||
@ -1004,10 +1080,6 @@ public final class ServerState {
|
||||
memoizedIsInitialized = 0;
|
||||
return false;
|
||||
}
|
||||
if (!hasClientOutput()) {
|
||||
memoizedIsInitialized = 0;
|
||||
return false;
|
||||
}
|
||||
if (!hasMyKey()) {
|
||||
memoizedIsInitialized = 0;
|
||||
return false;
|
||||
@ -1037,6 +1109,12 @@ public final class ServerState {
|
||||
if (((bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
output.writeBytes(6, myKey_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
output.writeUInt32(7, majorVersion_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000080) == 0x00000080)) {
|
||||
output.writeBytes(8, clientKey_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@ -1070,6 +1148,14 @@ public final class ServerState {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(6, myKey_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt32Size(7, majorVersion_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000080) == 0x00000080)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(8, clientKey_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@ -1203,6 +1289,10 @@ public final class ServerState {
|
||||
bitField0_ = (bitField0_ & ~0x00000010);
|
||||
myKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000020);
|
||||
majorVersion_ = 1;
|
||||
bitField0_ = (bitField0_ & ~0x00000040);
|
||||
clientKey_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000080);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -1255,6 +1345,14 @@ public final class ServerState {
|
||||
to_bitField0_ |= 0x00000020;
|
||||
}
|
||||
result.myKey_ = myKey_;
|
||||
if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
to_bitField0_ |= 0x00000040;
|
||||
}
|
||||
result.majorVersion_ = majorVersion_;
|
||||
if (((from_bitField0_ & 0x00000080) == 0x00000080)) {
|
||||
to_bitField0_ |= 0x00000080;
|
||||
}
|
||||
result.clientKey_ = clientKey_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
@ -1289,6 +1387,12 @@ public final class ServerState {
|
||||
if (other.hasMyKey()) {
|
||||
setMyKey(other.getMyKey());
|
||||
}
|
||||
if (other.hasMajorVersion()) {
|
||||
setMajorVersion(other.getMajorVersion());
|
||||
}
|
||||
if (other.hasClientKey()) {
|
||||
setClientKey(other.getClientKey());
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
return this;
|
||||
}
|
||||
@ -1306,10 +1410,6 @@ public final class ServerState {
|
||||
|
||||
return false;
|
||||
}
|
||||
if (!hasClientOutput()) {
|
||||
|
||||
return false;
|
||||
}
|
||||
if (!hasMyKey()) {
|
||||
|
||||
return false;
|
||||
@ -1472,19 +1572,19 @@ public final class ServerState {
|
||||
|
||||
private com.google.protobuf.ByteString clientOutput_ = com.google.protobuf.ByteString.EMPTY;
|
||||
/**
|
||||
* <code>required bytes clientOutput = 5;</code>
|
||||
* <code>optional bytes clientOutput = 5;</code>
|
||||
*/
|
||||
public boolean hasClientOutput() {
|
||||
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||
}
|
||||
/**
|
||||
* <code>required bytes clientOutput = 5;</code>
|
||||
* <code>optional bytes clientOutput = 5;</code>
|
||||
*/
|
||||
public com.google.protobuf.ByteString getClientOutput() {
|
||||
return clientOutput_;
|
||||
}
|
||||
/**
|
||||
* <code>required bytes clientOutput = 5;</code>
|
||||
* <code>optional bytes clientOutput = 5;</code>
|
||||
*/
|
||||
public Builder setClientOutput(com.google.protobuf.ByteString value) {
|
||||
if (value == null) {
|
||||
@ -1496,7 +1596,7 @@ public final class ServerState {
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* <code>required bytes clientOutput = 5;</code>
|
||||
* <code>optional bytes clientOutput = 5;</code>
|
||||
*/
|
||||
public Builder clearClientOutput() {
|
||||
bitField0_ = (bitField0_ & ~0x00000010);
|
||||
@ -1540,6 +1640,89 @@ public final class ServerState {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -1573,13 +1756,14 @@ public final class ServerState {
|
||||
"\n storedserverpaymentchannel.proto\022\017paym" +
|
||||
"entchannels\"\\\n\033StoredServerPaymentChanne" +
|
||||
"ls\022=\n\010channels\030\001 \003(\0132+.paymentchannels.S" +
|
||||
"toredServerPaymentChannel\"\272\001\n\032StoredServ" +
|
||||
"toredServerPaymentChannel\"\346\001\n\032StoredServ" +
|
||||
"erPaymentChannel\022\025\n\rbestValueToMe\030\001 \002(\004\022" +
|
||||
"\032\n\022bestValueSignature\030\002 \001(\014\022\'\n\037refundTra" +
|
||||
"nsactionUnlockTimeSecs\030\003 \002(\004\022\033\n\023contract" +
|
||||
"Transaction\030\004 \002(\014\022\024\n\014clientOutput\030\005 \002(\014\022" +
|
||||
"\r\n\005myKey\030\006 \002(\014B.\n\037org.bitcoinj.protocols" +
|
||||
".channelsB\013ServerState"
|
||||
"Transaction\030\004 \002(\014\022\024\n\014clientOutput\030\005 \001(\014\022" +
|
||||
"\r\n\005myKey\030\006 \002(\014\022\027\n\014majorVersion\030\007 \001(\r:\0011\022" +
|
||||
"\021\n\tclientKey\030\010 \001(\014B.\n\037org.bitcoinj.proto",
|
||||
"cols.channelsB\013ServerState"
|
||||
};
|
||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||
new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() {
|
||||
@ -1604,7 +1788,7 @@ public final class ServerState {
|
||||
internal_static_paymentchannels_StoredServerPaymentChannel_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_paymentchannels_StoredServerPaymentChannel_descriptor,
|
||||
new java.lang.String[] { "BestValueToMe", "BestValueSignature", "RefundTransactionUnlockTimeSecs", "ContractTransaction", "ClientOutput", "MyKey", });
|
||||
new java.lang.String[] { "BestValueToMe", "BestValueSignature", "RefundTransactionUnlockTimeSecs", "ContractTransaction", "ClientOutput", "MyKey", "MajorVersion", "ClientKey", });
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(outer_class_scope)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -17,6 +17,8 @@
|
||||
package org.bitcoinj.protocols.channels;
|
||||
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.HashMultimap;
|
||||
@ -279,14 +281,18 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
|
||||
(!params.hasMaxMoney() || channel.refundFees.compareTo(params.getMaxMoney()) <= 0));
|
||||
checkNotNull(channel.myKey.getPubKey());
|
||||
checkState(channel.refund.getConfidence().getSource() == TransactionConfidence.Source.SELF);
|
||||
checkNotNull(channel.myKey.getPubKey());
|
||||
final ClientState.StoredClientPaymentChannel.Builder value = ClientState.StoredClientPaymentChannel.newBuilder()
|
||||
.setMajorVersion(channel.majorVersion)
|
||||
.setId(ByteString.copyFrom(channel.id.getBytes()))
|
||||
.setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize()))
|
||||
.setRefundFees(channel.refundFees.value)
|
||||
.setRefundTransaction(ByteString.copyFrom(channel.refund.bitcoinSerialize()))
|
||||
.setMyKey(ByteString.copyFrom(new byte[0])) // Not used, but protobuf message requires
|
||||
.setMyPublicKey(ByteString.copyFrom(channel.myKey.getPubKey()))
|
||||
.setServerKey(ByteString.copyFrom(channel.serverKey.getPubKey()))
|
||||
.setValueToMe(channel.valueToMe.value)
|
||||
.setRefundFees(channel.refundFees.value);
|
||||
.setExpiryTime(channel.expiryTime);
|
||||
if (channel.close != null)
|
||||
value.setCloseTransactionHash(ByteString.copyFrom(channel.close.getHash().getBytes()));
|
||||
builder.addChannels(value);
|
||||
@ -311,12 +317,17 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
|
||||
ECKey myKey = (storedState.getMyKey().isEmpty()) ?
|
||||
containingWallet.findKeyFromPubKey(storedState.getMyPublicKey().toByteArray()) :
|
||||
ECKey.fromPrivate(storedState.getMyKey().toByteArray());
|
||||
StoredClientChannel channel = new StoredClientChannel(Sha256Hash.wrap(storedState.getId().toByteArray()),
|
||||
ECKey serverKey = storedState.hasServerKey() ? ECKey.fromPublicOnly(storedState.getServerKey().toByteArray()) : null;
|
||||
StoredClientChannel channel = new StoredClientChannel(storedState.getMajorVersion(),
|
||||
Sha256Hash.wrap(storedState.getId().toByteArray()),
|
||||
params.getDefaultSerializer().makeTransaction(storedState.getContractTransaction().toByteArray()),
|
||||
refundTransaction,
|
||||
myKey,
|
||||
serverKey,
|
||||
Coin.valueOf(storedState.getValueToMe()),
|
||||
Coin.valueOf(storedState.getRefundFees()), false);
|
||||
Coin.valueOf(storedState.getRefundFees()),
|
||||
storedState.getExpiryTime(),
|
||||
false);
|
||||
if (storedState.hasCloseTransactionHash()) {
|
||||
Sha256Hash closeTxHash = Sha256Hash.wrap(storedState.getCloseTransactionHash().toByteArray());
|
||||
channel.close = containingWallet.getTransaction(closeTxHash);
|
||||
@ -352,29 +363,43 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
|
||||
* when they expire.
|
||||
*/
|
||||
class StoredClientChannel {
|
||||
int majorVersion;
|
||||
Sha256Hash id;
|
||||
Transaction contract, refund;
|
||||
// The expiry time of the contract in protocol v2.
|
||||
long expiryTime;
|
||||
// The transaction that closed the channel (generated by the server)
|
||||
Transaction close;
|
||||
ECKey myKey;
|
||||
ECKey serverKey;
|
||||
Coin valueToMe, refundFees;
|
||||
|
||||
// In-memory flag to indicate intent to resume this channel (or that the channel is already in use)
|
||||
boolean active = false;
|
||||
|
||||
StoredClientChannel(Sha256Hash id, Transaction contract, Transaction refund, ECKey myKey, Coin valueToMe,
|
||||
Coin refundFees, boolean active) {
|
||||
StoredClientChannel(int majorVersion, Sha256Hash id, Transaction contract, Transaction refund, ECKey myKey, ECKey serverKey, Coin valueToMe,
|
||||
Coin refundFees, long expiryTime, boolean active) {
|
||||
this.majorVersion = majorVersion;
|
||||
this.id = id;
|
||||
this.contract = contract;
|
||||
this.refund = refund;
|
||||
this.myKey = myKey;
|
||||
this.serverKey = serverKey;
|
||||
this.valueToMe = valueToMe;
|
||||
this.refundFees = refundFees;
|
||||
this.expiryTime = expiryTime;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
long expiryTimeSeconds() {
|
||||
return refund.getLockTime() + 60 * 5;
|
||||
switch (majorVersion) {
|
||||
case 1:
|
||||
return refund.getLockTime() + 60 * 5;
|
||||
case 2:
|
||||
return expiryTime + 60 * 5;
|
||||
default:
|
||||
throw new IllegalStateException("Invalid version");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -382,13 +407,16 @@ class StoredClientChannel {
|
||||
final String newline = String.format(Locale.US, "%n");
|
||||
final String closeStr = close == null ? "still open" : close.toString().replaceAll(newline, newline + " ");
|
||||
return String.format(Locale.US, "Stored client channel for server ID %s (%s)%n" +
|
||||
" Version: %d%n" +
|
||||
" Key: %s%n" +
|
||||
" Server key: %s%n" +
|
||||
" Value left: %s%n" +
|
||||
" Refund fees: %s%n" +
|
||||
" Expiry : %s%n" +
|
||||
" Contract: %s" +
|
||||
"Refund: %s" +
|
||||
"Close: %s",
|
||||
id, active ? "active" : "inactive", myKey, valueToMe, refundFees,
|
||||
id, active ? "active" : "inactive", majorVersion, myKey, serverKey, valueToMe, refundFees, expiryTime,
|
||||
contract.toString().replaceAll(newline, newline + " "),
|
||||
refund.toString().replaceAll(newline, newline + " "),
|
||||
closeStr);
|
||||
|
@ -91,7 +91,7 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
|
||||
|
||||
/**
|
||||
* <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>
|
||||
*
|
||||
* <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);
|
||||
checkNotNull(channel.myKey.getPrivKeyBytes());
|
||||
ServerState.StoredServerPaymentChannel.Builder channelBuilder = ServerState.StoredServerPaymentChannel.newBuilder()
|
||||
.setMajorVersion(channel.majorVersion)
|
||||
.setBestValueToMe(channel.bestValueToMe.value)
|
||||
.setRefundTransactionUnlockTimeSecs(channel.refundTransactionUnlockTimeSecs)
|
||||
.setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize()))
|
||||
.setClientOutput(ByteString.copyFrom(channel.clientOutput.bitcoinSerialize()))
|
||||
.setMyKey(ByteString.copyFrom(channel.myKey.getPrivKeyBytes()));
|
||||
if (channel.majorVersion == 1) {
|
||||
channelBuilder.setClientOutput(ByteString.copyFrom(channel.clientOutput.bitcoinSerialize()));
|
||||
} else {
|
||||
channelBuilder.setClientKey(ByteString.copyFrom(channel.clientKey.getPubKey()));
|
||||
}
|
||||
if (channel.bestValueSignature != null)
|
||||
channelBuilder.setBestValueSignature(ByteString.copyFrom(channel.bestValueSignature));
|
||||
builder.addChannels(channelBuilder);
|
||||
@ -246,11 +251,21 @@ public class StoredPaymentChannelServerStates implements WalletExtension {
|
||||
ServerState.StoredServerPaymentChannels states = ServerState.StoredServerPaymentChannels.parseFrom(data);
|
||||
NetworkParameters params = containingWallet.getParams();
|
||||
for (ServerState.StoredServerPaymentChannel storedState : states.getChannelsList()) {
|
||||
final int majorVersion = storedState.getMajorVersion();
|
||||
TransactionOutput clientOutput = null;
|
||||
ECKey clientKey = null;
|
||||
if (majorVersion == 1) {
|
||||
clientOutput = new TransactionOutput(params, null, storedState.getClientOutput().toByteArray(), 0);
|
||||
} else {
|
||||
clientKey = ECKey.fromPublicOnly(storedState.getClientKey().toByteArray());
|
||||
}
|
||||
StoredServerChannel channel = new StoredServerChannel(null,
|
||||
majorVersion,
|
||||
params.getDefaultSerializer().makeTransaction(storedState.getContractTransaction().toByteArray()),
|
||||
new TransactionOutput(params, null, storedState.getClientOutput().toByteArray(), 0),
|
||||
clientOutput,
|
||||
storedState.getRefundTransactionUnlockTimeSecs(),
|
||||
ECKey.fromPrivate(storedState.getMyKey().toByteArray()),
|
||||
clientKey,
|
||||
Coin.valueOf(storedState.getBestValueToMe()),
|
||||
storedState.hasBestValueSignature() ? storedState.getBestValueSignature().toByteArray() : null);
|
||||
putChannel(channel);
|
||||
|
@ -30,24 +30,32 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
* time approaches.
|
||||
*/
|
||||
public class StoredServerChannel {
|
||||
/**
|
||||
* Channel version number. Currently can only be version 1
|
||||
*/
|
||||
int majorVersion;
|
||||
Coin bestValueToMe;
|
||||
byte[] bestValueSignature;
|
||||
long refundTransactionUnlockTimeSecs;
|
||||
Transaction contract;
|
||||
TransactionOutput clientOutput;
|
||||
ECKey myKey;
|
||||
// Used in protocol v2 only
|
||||
ECKey clientKey;
|
||||
|
||||
// In-memory pointer to the event handler which handles this channel if the client is connected.
|
||||
// Used as a flag to prevent duplicate connections and to disconnect the channel if its expire time approaches.
|
||||
private PaymentChannelServer connectedHandler = null;
|
||||
PaymentChannelServerState state = null;
|
||||
|
||||
StoredServerChannel(@Nullable PaymentChannelServerState state, Transaction contract, TransactionOutput clientOutput,
|
||||
long refundTransactionUnlockTimeSecs, ECKey myKey, Coin bestValueToMe, @Nullable byte[] bestValueSignature) {
|
||||
StoredServerChannel(@Nullable PaymentChannelServerState state, int majorVersion, Transaction contract, TransactionOutput clientOutput,
|
||||
long refundTransactionUnlockTimeSecs, ECKey myKey, ECKey clientKey, Coin bestValueToMe, @Nullable byte[] bestValueSignature) {
|
||||
this.majorVersion = majorVersion;
|
||||
this.contract = contract;
|
||||
this.clientOutput = clientOutput;
|
||||
this.refundTransactionUnlockTimeSecs = refundTransactionUnlockTimeSecs;
|
||||
this.myKey = myKey;
|
||||
this.clientKey = clientKey;
|
||||
this.bestValueToMe = bestValueToMe;
|
||||
this.bestValueSignature = bestValueSignature;
|
||||
this.state = state;
|
||||
@ -96,8 +104,18 @@ public class StoredServerChannel {
|
||||
* @param broadcaster The {@link TransactionBroadcaster} which will be used to broadcast contract/payment transactions.
|
||||
*/
|
||||
public synchronized PaymentChannelServerState getOrCreateState(Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException {
|
||||
if (state == null)
|
||||
state = new PaymentChannelServerState(this, wallet, broadcaster);
|
||||
if (state == null) {
|
||||
switch (majorVersion) {
|
||||
case 1:
|
||||
state = new PaymentChannelV1ServerState(this, wallet, broadcaster);
|
||||
break;
|
||||
case 2:
|
||||
state = new PaymentChannelV2ServerState(this, wallet, broadcaster);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Invalid version number found");
|
||||
}
|
||||
}
|
||||
checkArgument(wallet == state.wallet);
|
||||
return state;
|
||||
}
|
||||
@ -106,12 +124,13 @@ public class StoredServerChannel {
|
||||
public synchronized String toString() {
|
||||
final String newline = String.format(Locale.US, "%n");
|
||||
return String.format(Locale.US, "Stored server channel (%s)%n" +
|
||||
" Version: %d%n" +
|
||||
" Key: %s%n" +
|
||||
" Value to me: %s%n" +
|
||||
" Client output: %s%n" +
|
||||
" Refund unlock: %s (%d unix time)%n" +
|
||||
" Contract: %s%n",
|
||||
connectedHandler != null ? "connected" : "disconnected", myKey, bestValueToMe,
|
||||
connectedHandler != null ? "connected" : "disconnected", majorVersion, myKey, bestValueToMe,
|
||||
clientOutput, new Date(refundTransactionUnlockTimeSecs * 1000), refundTransactionUnlockTimeSecs,
|
||||
contract.toString().replaceAll(newline, newline + " "));
|
||||
}
|
||||
|
@ -457,10 +457,31 @@ public class ScriptBuilder {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Script createCLTVPaymentChannelInput(TransactionSignature from, TransactionSignature to) {
|
||||
public static Script createCLTVPaymentChannelP2SHRefund(TransactionSignature signature, Script redeemScript) {
|
||||
ScriptBuilder builder = new ScriptBuilder();
|
||||
builder.data(from.encodeToBitcoin());
|
||||
builder.data(to.encodeToBitcoin());
|
||||
builder.data(signature.encodeToBitcoin());
|
||||
builder.data(new byte[] { 0 }); // Use the CHECKLOCKTIMEVERIFY if branch
|
||||
builder.data(redeemScript.getProgram());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Script createCLTVPaymentChannelP2SHInput(byte[] from, byte[] to, Script redeemScript) {
|
||||
ScriptBuilder builder = new ScriptBuilder();
|
||||
builder.data(from);
|
||||
builder.data(to);
|
||||
builder.smallNum(1); // Use the CHECKLOCKTIMEVERIFY if branch
|
||||
builder.data(redeemScript.getProgram());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Script createCLTVPaymentChannelInput(TransactionSignature from, TransactionSignature to) {
|
||||
return createCLTVPaymentChannelInput(from.encodeToBitcoin(), to.encodeToBitcoin());
|
||||
}
|
||||
|
||||
public static Script createCLTVPaymentChannelInput(byte[] from, byte[] to) {
|
||||
ScriptBuilder builder = new ScriptBuilder();
|
||||
builder.data(from);
|
||||
builder.data(to);
|
||||
builder.smallNum(1); // Use the CHECKLOCKTIMEVERIFY if branch
|
||||
return builder.build();
|
||||
}
|
||||
|
@ -178,11 +178,17 @@ message ReturnRefund {
|
||||
// Sent from the primary to the secondary to complete initialization.
|
||||
message ProvideContract {
|
||||
// The serialized bytes of the transaction in Satoshi format.
|
||||
// For version 1:
|
||||
// * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
// necessary fees) TODO: tell the client how much fee it needs
|
||||
// * Its first output must be a 2-of-2 multisig output with the first pubkey being the
|
||||
// primary's and the second being the secondary's (ie the script must be exactly "OP_2
|
||||
// ProvideRefund.multisig_key Initiate.multisig_key OP_2 OP_CHECKMULTISIG")
|
||||
// For version 2:
|
||||
// * It must be signed and completely valid and ready for broadcast (ie it includes the
|
||||
// necessary fees) TODO: tell the client how much fee it needs
|
||||
// * Its first output must be a CHECKLOCKTIMEVERIFY output with the first pubkey being the
|
||||
// primary's and the second being the secondary's.
|
||||
required bytes tx = 1;
|
||||
|
||||
// To open the channel, an initial payment of the server-specified dust limit value must be
|
||||
@ -190,6 +196,12 @@ message ProvideContract {
|
||||
// no payment tx having been provided at all, or a payment that is smaller than the dust
|
||||
// limit being provided.
|
||||
required UpdatePayment initial_payment = 2;
|
||||
|
||||
// This field is added in protocol version 2 to send the client public key to the server.
|
||||
// In version 1 it isn't used.
|
||||
// This must be a raw pubkey in regular ECDSA form. Both compressed and non-compressed forms
|
||||
// are accepted. It is only used in the creation of the multisig contract.
|
||||
optional bytes client_key = 3;
|
||||
}
|
||||
|
||||
// This message can only be used by the primary after it has received a CHANNEL_OPEN message. It
|
||||
|
@ -43,9 +43,15 @@ message StoredClientPaymentChannel {
|
||||
// Deprecated, key is already stored in the wallet, and found using myPublicKey;
|
||||
required bytes myKey = 4;
|
||||
required uint64 valueToMe = 5;
|
||||
// Fees required to refund the transaction.
|
||||
required uint64 refundFees = 6;
|
||||
// When set, the hash of the transaction that was presented by the server for closure of the channel.
|
||||
// It spends the contractTransaction and is expected to be broadcast to the network by the server.
|
||||
// It's supposed to be in the wallet already.
|
||||
optional bytes closeTransactionHash = 7;
|
||||
// When set, the hash of the transaction that was presented by the server for closure of the channel.
|
||||
// It spends the contractTransaction and is expected to be broadcast to the network by the server.
|
||||
// It's supposed to be in the wallet already.
|
||||
optional bytes closeTransactionHash = 7;
|
||||
optional uint32 majorVersion = 9 [default = 1];
|
||||
// The expiry time of the CLTV lock. Only used in protocol v2.
|
||||
optional uint64 expiryTime = 10;
|
||||
// The server's public key. Only used in protocol v2.
|
||||
optional bytes serverKey = 11;
|
||||
}
|
@ -39,6 +39,9 @@ message StoredServerPaymentChannel {
|
||||
optional bytes bestValueSignature = 2;
|
||||
required uint64 refundTransactionUnlockTimeSecs = 3;
|
||||
required bytes contractTransaction = 4;
|
||||
required bytes clientOutput = 5;
|
||||
optional bytes clientOutput = 5;
|
||||
required bytes myKey = 6;
|
||||
optional uint32 majorVersion = 7 [default = 1];
|
||||
// Protocol version 2 only - the P2SH hash doesn't contain the required key
|
||||
optional bytes clientKey = 8;
|
||||
}
|
@ -29,16 +29,19 @@ import org.bitcoin.paymentchannel.Protos;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.lang.model.type.ExecutableType;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@ -48,6 +51,7 @@ import static org.bitcoinj.testing.FakeTxBuilder.createFakeBlock;
|
||||
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class ChannelConnectionTest extends TestWithWallet {
|
||||
private static final int CLIENT_MAJOR_VERSION = 1;
|
||||
private Wallet serverWallet;
|
||||
@ -64,6 +68,35 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* We use parameterized tests to run the channel connection tests with each
|
||||
* version of the channel.
|
||||
*/
|
||||
@Parameterized.Parameters(name = "{index}: ChannelConnectionTest({0})")
|
||||
public static Collection<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
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
@ -132,7 +165,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
|
||||
public void exectuteSimpleChannelTest(KeyParameter userKeySetup) throws Exception {
|
||||
// Test with network code and without any issues. We'll broadcast two txns: multisig contract and settle transaction.
|
||||
final SettableFuture<ListenableFuture<PaymentChannelServerState>> serverCloseFuture = SettableFuture.create();
|
||||
final SettableFuture<ListenableFuture<PaymentChannelV1ServerState>> serverCloseFuture = SettableFuture.create();
|
||||
final SettableFuture<Sha256Hash> channelOpenFuture = SettableFuture.create();
|
||||
final BlockingQueue<ChannelTestUtils.UpdatePair> q = new LinkedBlockingQueue<ChannelTestUtils.UpdatePair>();
|
||||
final PaymentChannelServerListener server = new PaymentChannelServerListener(mockBroadcaster, serverWallet, 30, COIN,
|
||||
@ -162,7 +195,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
server.bindAndStart(4243);
|
||||
|
||||
PaymentChannelClientConnection client = new PaymentChannelClientConnection(
|
||||
new InetSocketAddress("localhost", 4243), 30, wallet, myKey, COIN, "", PaymentChannelClient.DEFAULT_TIME_WINDOW, userKeySetup);
|
||||
new InetSocketAddress("localhost", 4243), 30, wallet, myKey, COIN, "", PaymentChannelClient.DEFAULT_TIME_WINDOW, userKeySetup, versionSelector);
|
||||
|
||||
// Wait for the multi-sig tx to be transmitted.
|
||||
broadcastTxPause.release();
|
||||
@ -213,6 +246,10 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
|
||||
broadcastTxPause.release();
|
||||
Transaction settleTx = broadcasts.take();
|
||||
assertTrue(serverState.getState() == PaymentChannelServerState.State.CLOSING ||
|
||||
serverState.getState() == PaymentChannelServerState.State.CLOSED);
|
||||
// Wait for the server thread to catch up with closing
|
||||
serverState.close().get();
|
||||
assertEquals(PaymentChannelServerState.State.CLOSED, serverState.getState());
|
||||
if (!serverState.getBestValueToMe().equals(amount) || !serverState.getFeePaid().equals(Coin.ZERO))
|
||||
fail();
|
||||
@ -220,7 +257,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
|
||||
// Send the settle TX to the client wallet.
|
||||
sendMoneyToWallet(settleTx, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(PaymentChannelClientState.State.CLOSED, client.state().getState());
|
||||
assertTrue(client.state().getState() == PaymentChannelClientState.State.CLOSED);
|
||||
|
||||
server.close();
|
||||
server.close();
|
||||
@ -235,9 +272,13 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
|
||||
@Test
|
||||
public void testServerErrorHandling_badTransaction() throws Exception {
|
||||
if (!useRefunds()) {
|
||||
// This test only applies to versions with refunds
|
||||
return;
|
||||
}
|
||||
// Gives the server crap and checks proper error responses are sent.
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
PaymentChannelServer server = pair.server;
|
||||
server.connectionOpen();
|
||||
client.connectionOpen();
|
||||
@ -262,7 +303,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
public void testServerErrorHandling_killSocketOnClose() throws Exception {
|
||||
// Make sure the server closes the socket on CLOSE
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
PaymentChannelServer server = pair.server;
|
||||
server.connectionOpen();
|
||||
client.connectionOpen();
|
||||
@ -280,7 +321,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
public void testServerErrorHandling_killSocketOnError() throws Exception {
|
||||
// Make sure the server closes the socket on ERROR
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
PaymentChannelServer server = pair.server;
|
||||
server.connectionOpen();
|
||||
client.connectionOpen();
|
||||
@ -304,7 +345,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
// Open up a normal channel.
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
pair.server.connectionOpen();
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
|
||||
PaymentChannelServer server = pair.server;
|
||||
client.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
@ -312,8 +353,10 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
final Protos.TwoWayChannelMessage initiateMsg = pair.serverRecorder.checkNextMsg(MessageType.INITIATE);
|
||||
Coin minPayment = Coin.valueOf(initiateMsg.getInitiate().getMinPayment());
|
||||
client.receiveMessage(initiateMsg);
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
|
||||
if (useRefunds()) {
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
|
||||
}
|
||||
broadcastTxPause.release();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
|
||||
broadcasts.take();
|
||||
@ -357,7 +400,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
// Open up a normal channel.
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
pair.server.connectionOpen();
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
|
||||
PaymentChannelServer server = pair.server;
|
||||
client.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
@ -365,8 +408,10 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
final Protos.TwoWayChannelMessage initiateMsg = pair.serverRecorder.checkNextMsg(MessageType.INITIATE);
|
||||
Coin minPayment = Coin.valueOf(initiateMsg.getInitiate().getMinPayment());
|
||||
client.receiveMessage(initiateMsg);
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
|
||||
if (useRefunds()) {
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
|
||||
}
|
||||
broadcastTxPause.release();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
|
||||
broadcasts.take();
|
||||
@ -415,7 +460,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
(StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
|
||||
|
||||
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder);
|
||||
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
|
||||
server = pair.server;
|
||||
client.connectionOpen();
|
||||
server.connectionOpen();
|
||||
@ -442,7 +487,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
|
||||
// Now open up a new client with the same id and make sure the server disconnects the previous client.
|
||||
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder);
|
||||
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
|
||||
server = pair.server;
|
||||
client.connectionOpen();
|
||||
server.connectionOpen();
|
||||
@ -454,7 +499,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
}
|
||||
// Make sure the server allows two simultaneous opens. It will close the first and allow resumption of the second.
|
||||
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder);
|
||||
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
|
||||
server = pair.server;
|
||||
client.connectionOpen();
|
||||
server.connectionOpen();
|
||||
@ -484,7 +529,11 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
StoredPaymentChannelClientStates newClientStates = new StoredPaymentChannelClientStates(wallet, mockBroadcaster);
|
||||
newClientStates.deserializeWalletExtension(wallet, clientStoredChannels.serializeWalletExtension());
|
||||
broadcastTxPause.release();
|
||||
assertTrue(broadcasts.take().getOutput(0).getScriptPubKey().isSentToMultiSig());
|
||||
if (isMultiSigContract()) {
|
||||
assertTrue(broadcasts.take().getOutput(0).getScriptPubKey().isSentToMultiSig());
|
||||
} else {
|
||||
assertTrue(broadcasts.take().getOutput(0).getScriptPubKey().isPayToScriptHash());
|
||||
}
|
||||
broadcastTxPause.release();
|
||||
assertEquals(TransactionConfidence.Source.SELF, broadcasts.take().getConfidence().getSource());
|
||||
assertTrue(broadcasts.isEmpty());
|
||||
@ -534,7 +583,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
public void testClientUnknownVersion() throws Exception {
|
||||
// Tests client rejects unknown version
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
client.connectionOpen();
|
||||
pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
|
||||
client.receiveMessage(Protos.TwoWayChannelMessage.newBuilder()
|
||||
@ -554,7 +603,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
// Tests that clients reject too large time windows
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster, 100);
|
||||
PaymentChannelServer server = pair.server;
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
client.connectionOpen();
|
||||
server.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
@ -579,7 +628,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
public void testValuesAreRespected() throws Exception {
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
PaymentChannelServer server = pair.server;
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
client.connectionOpen();
|
||||
server.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
@ -605,7 +654,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
server = pair.server;
|
||||
final Coin myValue = COIN.multiply(10);
|
||||
client = new PaymentChannelClient(wallet, myKey, myValue, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
client = new PaymentChannelClient(wallet, myKey, myValue, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
client.connectionOpen();
|
||||
server.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
@ -616,9 +665,15 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
.setMultisigKey(ByteString.copyFrom(new ECKey().getPubKey()))
|
||||
.setMinPayment(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value))
|
||||
.setType(MessageType.INITIATE).build());
|
||||
final Protos.TwoWayChannelMessage provideRefund = pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND);
|
||||
Transaction refund = new Transaction(params, provideRefund.getProvideRefund().getTx().toByteArray());
|
||||
assertEquals(myValue, refund.getOutput(0).getValue());
|
||||
if (useRefunds()) {
|
||||
final Protos.TwoWayChannelMessage provideRefund = pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND);
|
||||
Transaction refund = new Transaction(params, provideRefund.getProvideRefund().getTx().toByteArray());
|
||||
assertEquals(myValue, refund.getOutput(0).getValue());
|
||||
} else {
|
||||
assertEquals(2, client.state().getMajorVersion());
|
||||
PaymentChannelV2ClientState state = (PaymentChannelV2ClientState) client.state();
|
||||
assertEquals(myValue, state.refundTx.getOutput(0).getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -627,7 +682,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
emptyWallet.freshReceiveKey();
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
PaymentChannelServer server = pair.server;
|
||||
PaymentChannelClient client = new PaymentChannelClient(emptyWallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(emptyWallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
client.connectionOpen();
|
||||
server.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
@ -649,7 +704,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
public void testClientRefusesNonCanonicalKey() throws Exception {
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
PaymentChannelServer server = pair.server;
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
client.connectionOpen();
|
||||
server.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
@ -667,7 +722,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
public void testClientResumeNothing() throws Exception {
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
PaymentChannelServer server = pair.server;
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
client.connectionOpen();
|
||||
server.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
@ -681,7 +736,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
@Test
|
||||
public void testClientRandomMessage() throws Exception {
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, Sha256Hash.ZERO_HASH, pair.clientRecorder, versionSelector);
|
||||
|
||||
client.connectionOpen();
|
||||
pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
|
||||
@ -702,14 +757,16 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
pair.server.connectionOpen();
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
|
||||
PaymentChannelServer server = pair.server;
|
||||
client.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
|
||||
if (useRefunds()) {
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
|
||||
}
|
||||
broadcastTxPause.release();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
|
||||
broadcasts.take();
|
||||
@ -733,7 +790,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
client.connectionClosed();
|
||||
|
||||
// Now try opening a new channel with the same server ID and verify the client asks for a new channel.
|
||||
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder);
|
||||
client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
|
||||
client.connectionOpen();
|
||||
Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
|
||||
assertFalse(msg.getClientVersion().hasPreviousChannelContractHash());
|
||||
@ -748,14 +805,16 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
pair.server.connectionOpen();
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
|
||||
PaymentChannelServer server = pair.server;
|
||||
client.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
|
||||
if (useRefunds()) {
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
|
||||
}
|
||||
broadcastTxPause.release();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
|
||||
broadcasts.take();
|
||||
@ -800,7 +859,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
pair.server.connectionOpen();
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
|
||||
PaymentChannelServer server = pair.server;
|
||||
client.connectionOpen();
|
||||
final Protos.TwoWayChannelMessage msg = pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION);
|
||||
@ -808,8 +867,10 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
server.receiveMessage(msg);
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.SERVER_VERSION));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.INITIATE));
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
|
||||
if (useRefunds()) {
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_REFUND));
|
||||
client.receiveMessage(pair.serverRecorder.checkNextMsg(MessageType.RETURN_REFUND));
|
||||
}
|
||||
broadcastTxPause.release();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.PROVIDE_CONTRACT));
|
||||
broadcasts.take();
|
||||
@ -829,7 +890,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
||||
Sha256Hash someServerId = Sha256Hash.ZERO_HASH;
|
||||
ChannelTestUtils.RecordingPair pair = ChannelTestUtils.makeRecorders(serverWallet, mockBroadcaster);
|
||||
pair.server.connectionOpen();
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder);
|
||||
PaymentChannelClient client = new PaymentChannelClient(wallet, myKey, COIN, someServerId, pair.clientRecorder, versionSelector);
|
||||
PaymentChannelServer server = pair.server;
|
||||
client.connectionOpen();
|
||||
server.receiveMessage(pair.clientRecorder.checkNextMsg(MessageType.CLIENT_VERSION));
|
||||
|
@ -6,8 +6,12 @@ import org.easymock.Capture;
|
||||
import org.easymock.EasyMock;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
|
||||
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage;
|
||||
@ -17,9 +21,9 @@ import static org.easymock.EasyMock.createMock;
|
||||
import static org.easymock.EasyMock.replay;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class PaymentChannelClientTest {
|
||||
|
||||
private static final int CLIENT_MAJOR_VERSION = 1;
|
||||
private Wallet wallet;
|
||||
private ECKey ecKey;
|
||||
private Sha256Hash serverHash;
|
||||
@ -28,6 +32,22 @@ public class PaymentChannelClientTest {
|
||||
public Capture<TwoWayChannelMessage> clientVersionCapture;
|
||||
public int defaultTimeWindow = 86340;
|
||||
|
||||
/**
|
||||
* We use parameterized tests to run the client channel tests with each
|
||||
* version of the channel.
|
||||
*/
|
||||
@Parameterized.Parameters(name = "{index}: PaymentChannelClientTest({0})")
|
||||
public static Collection<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
|
||||
public void before() {
|
||||
wallet = createMock(Wallet.class);
|
||||
@ -40,7 +60,7 @@ public class PaymentChannelClientTest {
|
||||
|
||||
@Test
|
||||
public void shouldSendClientVersionOnChannelOpen() throws Exception {
|
||||
PaymentChannelClient dut = new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, connection);
|
||||
PaymentChannelClient dut = new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, connection, versionSelector);
|
||||
connection.sendToServer(capture(clientVersionCapture));
|
||||
EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap<String, WalletExtension>());
|
||||
replay(connection, wallet);
|
||||
@ -52,7 +72,7 @@ public class PaymentChannelClientTest {
|
||||
long timeWindow = 4000;
|
||||
KeyParameter userKey = null;
|
||||
PaymentChannelClient dut =
|
||||
new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, timeWindow, userKey, connection);
|
||||
new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, timeWindow, userKey, connection, versionSelector);
|
||||
connection.sendToServer(capture(clientVersionCapture));
|
||||
EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap<String, WalletExtension>());
|
||||
replay(connection, wallet);
|
||||
@ -66,7 +86,8 @@ public class PaymentChannelClientTest {
|
||||
assertEquals("Wrong type " + type, CLIENT_VERSION, type);
|
||||
final Protos.ClientVersion clientVersion = response.getClientVersion();
|
||||
final int major = clientVersion.getMajor();
|
||||
assertEquals("Wrong major version " + major, CLIENT_MAJOR_VERSION, major);
|
||||
final int requestedVersion = versionSelector.getRequestedMajorVersion();
|
||||
assertEquals("Wrong major version " + major, requestedVersion, major);
|
||||
final long actualTimeWindow = clientVersion.getTimeWindowSecs();
|
||||
assertEquals("Wrong timeWindow " + actualTimeWindow, expectedTimeWindow, actualTimeWindow );
|
||||
}
|
||||
|
@ -8,6 +8,11 @@ import org.bitcoin.paymentchannel.Protos;
|
||||
import org.easymock.Capture;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage;
|
||||
@ -15,10 +20,8 @@ import static org.bitcoin.paymentchannel.Protos.TwoWayChannelMessage.MessageType
|
||||
import static org.easymock.EasyMock.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class PaymentChannelServerTest {
|
||||
|
||||
private static final int CLIENT_MAJOR_VERSION = 1;
|
||||
private static final long SERVER_MAJOR_VERSION = 1;
|
||||
public Wallet wallet;
|
||||
public PaymentChannelServer.ServerConnection connection;
|
||||
public PaymentChannelServer dut;
|
||||
@ -35,6 +38,17 @@ public class PaymentChannelServerTest {
|
||||
Utils.setMockClock();
|
||||
}
|
||||
|
||||
/**
|
||||
* We use parameterized tests to run the client channel tests with each
|
||||
* version of the channel.
|
||||
*/
|
||||
@Parameterized.Parameters(name = "{index}: PaymentChannelServerTest(version {0})")
|
||||
public static Collection<Integer> data() {
|
||||
return Arrays.asList(1, 2);
|
||||
}
|
||||
|
||||
@Parameterized.Parameter
|
||||
public int protocolVersion;
|
||||
|
||||
@Test
|
||||
public void shouldAcceptDefaultTimeWindow() {
|
||||
@ -123,7 +137,7 @@ public class PaymentChannelServerTest {
|
||||
final MessageType type = response.getType();
|
||||
assertEquals("Wrong type " + type, MessageType.SERVER_VERSION, type);
|
||||
final long major = response.getServerVersion().getMajor();
|
||||
assertEquals("Wrong major version", SERVER_MAJOR_VERSION, major);
|
||||
assertEquals("Wrong major version", protocolVersion, major);
|
||||
}
|
||||
|
||||
private void assertExpireTime(long expectedExpire, Capture<TwoWayChannelMessage> initiateCapture) {
|
||||
@ -136,12 +150,12 @@ public class PaymentChannelServerTest {
|
||||
}
|
||||
|
||||
private TwoWayChannelMessage createClientVersionMessage() {
|
||||
final Protos.ClientVersion.Builder clientVersion = Protos.ClientVersion.newBuilder().setMajor(CLIENT_MAJOR_VERSION);
|
||||
final Protos.ClientVersion.Builder clientVersion = Protos.ClientVersion.newBuilder().setMajor(protocolVersion);
|
||||
return TwoWayChannelMessage.newBuilder().setType(MessageType.CLIENT_VERSION).setClientVersion(clientVersion).build();
|
||||
}
|
||||
|
||||
private TwoWayChannelMessage createClientVersionMessage(long timeWindow) {
|
||||
final Protos.ClientVersion.Builder clientVersion = Protos.ClientVersion.newBuilder().setMajor(CLIENT_MAJOR_VERSION);
|
||||
final Protos.ClientVersion.Builder clientVersion = Protos.ClientVersion.newBuilder().setMajor(protocolVersion);
|
||||
if (timeWindow > 0) clientVersion.setTimeWindowSecs(timeWindow);
|
||||
return TwoWayChannelMessage.newBuilder().setType(MessageType.CLIENT_VERSION).setClientVersion(clientVersion).build();
|
||||
}
|
||||
|
@ -26,9 +26,12 @@ import com.google.common.util.concurrent.SettableFuture;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@ -39,6 +42,7 @@ import static org.bitcoinj.testing.FakeTxBuilder.createFakeTx;
|
||||
import static org.bitcoinj.testing.FakeTxBuilder.makeSolvedTestBlock;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class PaymentChannelStateTest extends TestWithWallet {
|
||||
private ECKey serverKey;
|
||||
private Coin halfCoin;
|
||||
@ -48,6 +52,27 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
private TransactionBroadcaster mockBroadcaster;
|
||||
private BlockingQueue<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 {
|
||||
Transaction tx;
|
||||
SettableFuture<Transaction> future;
|
||||
@ -61,6 +86,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
Utils.setMockClock(); // Use mock clock
|
||||
super.setUp();
|
||||
wallet.addExtension(new StoredPaymentChannelClientStates(wallet, new TransactionBroadcaster() {
|
||||
@Override
|
||||
@ -93,13 +119,93 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
private PaymentChannelClientState makeClientState(Wallet wallet, ECKey myKey, ECKey serverKey, Coin value, long time) {
|
||||
switch (versionSelector) {
|
||||
case VERSION_1:
|
||||
return new PaymentChannelV1ClientState(wallet, myKey, serverKey, value, time);
|
||||
case VERSION_2_ALLOW_1:
|
||||
case VERSION_2:
|
||||
return new PaymentChannelV2ClientState(wallet, myKey, serverKey, value, time);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentChannelServerState makeServerState(TransactionBroadcaster broadcaster, Wallet wallet, ECKey serverKey, long time) {
|
||||
switch (versionSelector) {
|
||||
case VERSION_1:
|
||||
return new PaymentChannelV1ServerState(broadcaster, wallet, serverKey, time);
|
||||
case VERSION_2_ALLOW_1:
|
||||
case VERSION_2:
|
||||
return new PaymentChannelV2ServerState(broadcaster, wallet, serverKey, time);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentChannelV1ClientState clientV1State() {
|
||||
if (clientState instanceof PaymentChannelV1ClientState) {
|
||||
return (PaymentChannelV1ClientState) clientState;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentChannelV1ServerState serverV1State() {
|
||||
if (serverState instanceof PaymentChannelV1ServerState) {
|
||||
return (PaymentChannelV1ServerState) serverState;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentChannelV2ClientState clientV2State() {
|
||||
if (clientState instanceof PaymentChannelV2ClientState) {
|
||||
return (PaymentChannelV2ClientState) clientState;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentChannelV2ServerState serverV2State() {
|
||||
if (serverState instanceof PaymentChannelV2ServerState) {
|
||||
return (PaymentChannelV2ServerState) serverState;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentChannelServerState.State getInitialServerState() {
|
||||
switch (versionSelector) {
|
||||
case VERSION_1:
|
||||
return PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION;
|
||||
case VERSION_2_ALLOW_1:
|
||||
case VERSION_2:
|
||||
return PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentChannelClientState.State getInitialClientState() {
|
||||
switch (versionSelector) {
|
||||
case VERSION_1:
|
||||
return PaymentChannelClientState.State.INITIATED;
|
||||
case VERSION_2_ALLOW_1:
|
||||
case VERSION_2:
|
||||
return PaymentChannelClientState.State.SAVE_STATE_IN_WALLET;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stateErrors() throws Exception {
|
||||
PaymentChannelClientState channelState = new PaymentChannelClientState(wallet, myKey, serverKey,
|
||||
PaymentChannelClientState channelState = makeClientState(wallet, myKey, serverKey,
|
||||
COIN.multiply(10), 20);
|
||||
assertEquals(PaymentChannelClientState.State.NEW, channelState.getState());
|
||||
try {
|
||||
channelState.getMultisigContract();
|
||||
channelState.getContract();
|
||||
fail();
|
||||
} catch (IllegalStateException e) {
|
||||
// Expected.
|
||||
@ -117,36 +223,48 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
Utils.setMockClock(); // Use mock clock
|
||||
final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24;
|
||||
|
||||
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
|
||||
serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(getInitialServerState(), serverState.getState());
|
||||
|
||||
clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
|
||||
clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
|
||||
clientState.initiate();
|
||||
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
|
||||
assertEquals(getInitialClientState(), clientState.getState());
|
||||
|
||||
// Send the refund tx from client to server and get back the signature.
|
||||
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientState.provideRefundSignature(refundSig, null);
|
||||
Transaction refund;
|
||||
if (useRefunds()) {
|
||||
refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientV1State().provideRefundSignature(refundSig, null);
|
||||
} else {
|
||||
refund = clientV2State().getRefundTransaction();
|
||||
}
|
||||
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
|
||||
clientState.fakeSave();
|
||||
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
|
||||
|
||||
// Validate the multisig contract looks right.
|
||||
Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize());
|
||||
Transaction multisigContract = new Transaction(params, clientState.getContract().bitcoinSerialize());
|
||||
assertEquals(PaymentChannelClientState.State.READY, clientState.getState());
|
||||
assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change.
|
||||
Script script = multisigContract.getOutput(0).getScriptPubKey();
|
||||
assertTrue(script.isSentToMultiSig());
|
||||
if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) {
|
||||
assertTrue(script.isSentToMultiSig());
|
||||
} else {
|
||||
assertTrue(script.isPayToScriptHash());
|
||||
}
|
||||
script = multisigContract.getOutput(1).getScriptPubKey();
|
||||
assertTrue(script.isSentToAddress());
|
||||
assertTrue(wallet.getPendingTransactions().contains(multisigContract));
|
||||
|
||||
// Provide the server with the multisig contract and simulate successful propagation/acceptance.
|
||||
serverState.provideMultiSigContract(multisigContract);
|
||||
if (!useRefunds()) {
|
||||
serverV2State().provideClientKey(clientState.myKey.getPubKey());
|
||||
}
|
||||
serverState.provideContract(multisigContract);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
|
||||
final TxFuturePair pair = broadcasts.take();
|
||||
pair.future.set(pair.tx);
|
||||
@ -156,12 +274,12 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
assertEquals(2, wallet.getTransactions(false).size());
|
||||
Iterator<Transaction> walletTransactionIterator = wallet.getTransactions(false).iterator();
|
||||
Transaction clientWalletMultisigContract = walletTransactionIterator.next();
|
||||
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash()));
|
||||
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash()));
|
||||
if (!clientWalletMultisigContract.getHash().equals(multisigContract.getHash())) {
|
||||
clientWalletMultisigContract = walletTransactionIterator.next();
|
||||
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash()));
|
||||
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash()));
|
||||
} else
|
||||
assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getCompletedRefundTransaction().getHash()));
|
||||
assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getRefundTransaction().getHash()));
|
||||
assertEquals(multisigContract.getHash(), clientWalletMultisigContract.getHash());
|
||||
assertFalse(clientWalletMultisigContract.getInput(0).getConnectedOutput().getSpentBy().getParentTransaction().getHash().equals(refund.getHash()));
|
||||
|
||||
@ -231,40 +349,49 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
Utils.setMockClock(); // Use mock clock
|
||||
final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24;
|
||||
|
||||
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
|
||||
serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(getInitialServerState(), serverState.getState());
|
||||
|
||||
clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()),
|
||||
clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()),
|
||||
CENT.divide(2), EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
|
||||
assertEquals(CENT.divide(2), clientState.getTotalValue());
|
||||
clientState.initiate();
|
||||
// We will have to pay min_tx_fee twice - both the multisig contract and the refund tx
|
||||
assertEquals(clientState.getRefundTxFees(), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(2));
|
||||
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
|
||||
assertEquals(getInitialClientState(), clientState.getState());
|
||||
|
||||
// Send the refund tx from client to server and get back the signature.
|
||||
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientState.provideRefundSignature(refundSig, null);
|
||||
if (useRefunds()) {
|
||||
// Send the refund tx from client to server and get back the signature.
|
||||
Transaction refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientV1State().provideRefundSignature(refundSig, null);
|
||||
}
|
||||
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
|
||||
clientState.fakeSave();
|
||||
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
|
||||
|
||||
// Validate the multisig contract looks right.
|
||||
Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize());
|
||||
Transaction multisigContract = new Transaction(params, clientState.getContract().bitcoinSerialize());
|
||||
assertEquals(PaymentChannelClientState.State.READY, clientState.getState());
|
||||
assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change.
|
||||
Script script = multisigContract.getOutput(0).getScriptPubKey();
|
||||
assertTrue(script.isSentToMultiSig());
|
||||
if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) {
|
||||
assertTrue(script.isSentToMultiSig());
|
||||
} else {
|
||||
assertTrue(script.isPayToScriptHash());
|
||||
}
|
||||
script = multisigContract.getOutput(1).getScriptPubKey();
|
||||
assertTrue(script.isSentToAddress());
|
||||
assertTrue(wallet.getPendingTransactions().contains(multisigContract));
|
||||
|
||||
// Provide the server with the multisig contract and simulate successful propagation/acceptance.
|
||||
serverState.provideMultiSigContract(multisigContract);
|
||||
if (!useRefunds()) {
|
||||
serverV2State().provideClientKey(clientState.myKey.getPubKey());
|
||||
}
|
||||
serverState.provideContract(multisigContract);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
|
||||
final TxFuturePair pop = broadcasts.take();
|
||||
pop.future.set(pop.tx);
|
||||
@ -301,7 +428,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
clientBroadcastedMultiSig.future.set(clientBroadcastedMultiSig.tx);
|
||||
|
||||
Transaction clientBroadcastedRefund = broadcastRefund.tx;
|
||||
assertEquals(clientBroadcastedRefund.getHash(), clientState.getCompletedRefundTransaction().getHash());
|
||||
assertEquals(clientBroadcastedRefund.getHash(), clientState.getRefundTransaction().getHash());
|
||||
for (TransactionInput input : clientBroadcastedRefund.getInputs()) {
|
||||
// If the multisig output is connected, the wallet will fail to deserialize
|
||||
if (input.getOutpoint().getHash().equals(clientBroadcastedMultiSig.tx.getHash()))
|
||||
@ -332,93 +459,122 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
Utils.setMockClock(); // Use mock clock
|
||||
final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24;
|
||||
|
||||
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
|
||||
serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(getInitialServerState(), serverState.getState());
|
||||
|
||||
clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
|
||||
clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
|
||||
clientState.initiate();
|
||||
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
|
||||
assertEquals(getInitialClientState(), clientState.getState());
|
||||
|
||||
// Test refund transaction with any number of issues
|
||||
byte[] refundTxBytes = clientState.getIncompleteRefundTransaction().bitcoinSerialize();
|
||||
Transaction refund = new Transaction(params, refundTxBytes);
|
||||
refund.addOutput(Coin.ZERO, new ECKey().toAddress(params));
|
||||
try {
|
||||
serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
fail();
|
||||
} catch (VerificationException e) {}
|
||||
if (useRefunds()) {
|
||||
// Test refund transaction with any number of issues
|
||||
byte[] refundTxBytes = clientV1State().getIncompleteRefundTransaction().bitcoinSerialize();
|
||||
Transaction refund = new Transaction(params, refundTxBytes);
|
||||
refund.addOutput(Coin.ZERO, new ECKey().toAddress(params));
|
||||
try {
|
||||
serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
}
|
||||
|
||||
refund = new Transaction(params, refundTxBytes);
|
||||
refund.addInput(new TransactionInput(params, refund, new byte[] {}, new TransactionOutPoint(params, 42, refund.getHash())));
|
||||
try {
|
||||
serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
fail();
|
||||
} catch (VerificationException e) {}
|
||||
refund = new Transaction(params, refundTxBytes);
|
||||
refund.addInput(new TransactionInput(params, refund, new byte[]{}, new TransactionOutPoint(params, 42, refund.getHash())));
|
||||
try {
|
||||
serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
}
|
||||
|
||||
refund = new Transaction(params, refundTxBytes);
|
||||
refund.setLockTime(0);
|
||||
try {
|
||||
serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
fail();
|
||||
} catch (VerificationException e) {}
|
||||
refund = new Transaction(params, refundTxBytes);
|
||||
refund.setLockTime(0);
|
||||
try {
|
||||
serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
}
|
||||
|
||||
refund = new Transaction(params, refundTxBytes);
|
||||
refund.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE);
|
||||
try {
|
||||
serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
fail();
|
||||
} catch (VerificationException e) {}
|
||||
refund = new Transaction(params, refundTxBytes);
|
||||
refund.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE);
|
||||
try {
|
||||
serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
}
|
||||
|
||||
refund = new Transaction(params, refundTxBytes);
|
||||
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
try { serverState.provideRefundTransaction(refund, myKey.getPubKey()); fail(); } catch (IllegalStateException e) {}
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
refund = new Transaction(params, refundTxBytes);
|
||||
byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
try {
|
||||
serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
fail();
|
||||
} catch (IllegalStateException e) {
|
||||
}
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
|
||||
byte[] refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
|
||||
refundSigCopy[refundSigCopy.length-1] = (byte) (Transaction.SigHash.NONE.ordinal() + 1);
|
||||
try {
|
||||
clientState.provideRefundSignature(refundSigCopy, null);
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
assertTrue(e.getMessage().contains("SIGHASH_NONE"));
|
||||
byte[] refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
|
||||
refundSigCopy[refundSigCopy.length - 1] = (byte) (Transaction.SigHash.NONE.ordinal() + 1);
|
||||
try {
|
||||
clientV1State().provideRefundSignature(refundSigCopy, null);
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
assertTrue(e.getMessage().contains("SIGHASH_NONE"));
|
||||
}
|
||||
|
||||
refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
|
||||
refundSigCopy[3] ^= 0x42; // Make the signature fail standard checks
|
||||
try {
|
||||
clientV1State().provideRefundSignature(refundSigCopy, null);
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
assertTrue(e.getMessage().contains("not canonical"));
|
||||
}
|
||||
|
||||
refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
|
||||
refundSigCopy[10] ^= 0x42; // Flip some random bits in the signature (to make it invalid, not just nonstandard)
|
||||
try {
|
||||
clientV1State().provideRefundSignature(refundSigCopy, null);
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
assertFalse(e.getMessage().contains("not canonical"));
|
||||
}
|
||||
|
||||
refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
|
||||
try {
|
||||
clientV1State().getCompletedRefundTransaction();
|
||||
fail();
|
||||
} catch (IllegalStateException e) {
|
||||
}
|
||||
clientV1State().provideRefundSignature(refundSigCopy, null);
|
||||
try {
|
||||
clientV1State().provideRefundSignature(refundSigCopy, null);
|
||||
fail();
|
||||
} catch (IllegalStateException e) {
|
||||
}
|
||||
}
|
||||
|
||||
refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
|
||||
refundSigCopy[3] ^= 0x42; // Make the signature fail standard checks
|
||||
try {
|
||||
clientState.provideRefundSignature(refundSigCopy, null);
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
assertTrue(e.getMessage().contains("not canonical"));
|
||||
}
|
||||
|
||||
refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
|
||||
refundSigCopy[10] ^= 0x42; // Flip some random bits in the signature (to make it invalid, not just nonstandard)
|
||||
try {
|
||||
clientState.provideRefundSignature(refundSigCopy, null);
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
assertFalse(e.getMessage().contains("not canonical"));
|
||||
}
|
||||
|
||||
refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
|
||||
try { clientState.getCompletedRefundTransaction(); fail(); } catch (IllegalStateException e) {}
|
||||
clientState.provideRefundSignature(refundSigCopy, null);
|
||||
try { clientState.provideRefundSignature(refundSigCopy, null); fail(); } catch (IllegalStateException e) {}
|
||||
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
|
||||
clientState.fakeSave();
|
||||
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
|
||||
|
||||
if (!useRefunds()) {
|
||||
serverV2State().provideClientKey(myKey.getPubKey());
|
||||
}
|
||||
|
||||
try { clientState.incrementPaymentBy(Coin.SATOSHI, null); fail(); } catch (IllegalStateException e) {}
|
||||
|
||||
byte[] multisigContractSerialized = clientState.getMultisigContract().bitcoinSerialize();
|
||||
byte[] multisigContractSerialized = clientState.getContract().bitcoinSerialize();
|
||||
|
||||
Transaction multisigContract = new Transaction(params, multisigContractSerialized);
|
||||
multisigContract.clearOutputs();
|
||||
multisigContract.addOutput(halfCoin, ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(serverKey, myKey)));
|
||||
// Swap order of client and server keys to check correct failure
|
||||
if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) {
|
||||
multisigContract.addOutput(halfCoin, ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(serverKey, myKey)));
|
||||
} else {
|
||||
multisigContract.addOutput(halfCoin,
|
||||
ScriptBuilder.createP2SHOutputScript(
|
||||
ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(serverState.getExpiryTime()), serverKey, myKey)));
|
||||
}
|
||||
try {
|
||||
serverState.provideMultiSigContract(multisigContract);
|
||||
serverState.provideContract(multisigContract);
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
assertTrue(e.getMessage().contains("client and server in that order"));
|
||||
@ -426,9 +582,15 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
|
||||
multisigContract = new Transaction(params, multisigContractSerialized);
|
||||
multisigContract.clearOutputs();
|
||||
multisigContract.addOutput(Coin.ZERO, ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(myKey, serverKey)));
|
||||
if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) {
|
||||
multisigContract.addOutput(Coin.ZERO, ScriptBuilder.createMultiSigOutputScript(2, Lists.newArrayList(myKey, serverKey)));
|
||||
} else {
|
||||
multisigContract.addOutput(Coin.ZERO,
|
||||
ScriptBuilder.createP2SHOutputScript(
|
||||
ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(serverState.getExpiryTime()), myKey, serverKey)));
|
||||
}
|
||||
try {
|
||||
serverState.provideMultiSigContract(multisigContract);
|
||||
serverState.provideContract(multisigContract);
|
||||
fail();
|
||||
} catch (VerificationException e) {
|
||||
assertTrue(e.getMessage().contains("zero value"));
|
||||
@ -438,13 +600,13 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
multisigContract.clearOutputs();
|
||||
multisigContract.addOutput(new TransactionOutput(params, multisigContract, halfCoin, new byte[] {0x01}));
|
||||
try {
|
||||
serverState.provideMultiSigContract(multisigContract);
|
||||
serverState.provideContract(multisigContract);
|
||||
fail();
|
||||
} catch (VerificationException e) {}
|
||||
|
||||
multisigContract = new Transaction(params, multisigContractSerialized);
|
||||
ListenableFuture<PaymentChannelServerState> multisigStateFuture = serverState.provideMultiSigContract(multisigContract);
|
||||
try { serverState.provideMultiSigContract(multisigContract); fail(); } catch (IllegalStateException e) {}
|
||||
ListenableFuture<PaymentChannelServerState> multisigStateFuture = serverState.provideContract(multisigContract);
|
||||
try { serverState.provideContract(multisigContract); fail(); } catch (IllegalStateException e) {}
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
|
||||
assertFalse(multisigStateFuture.isDone());
|
||||
final TxFuturePair pair = broadcasts.take();
|
||||
@ -536,18 +698,18 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
Utils.setMockClock(); // Use mock clock
|
||||
final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24;
|
||||
|
||||
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
|
||||
serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(getInitialServerState(), serverState.getState());
|
||||
|
||||
// Clearly SATOSHI is far too small to be useful
|
||||
clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), Coin.SATOSHI, EXPIRE_TIME);
|
||||
clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), Coin.SATOSHI, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
|
||||
try {
|
||||
clientState.initiate();
|
||||
fail();
|
||||
} catch (ValueOutOfRangeException e) {}
|
||||
|
||||
clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()),
|
||||
clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()),
|
||||
Transaction.MIN_NONDUST_OUTPUT.subtract(Coin.SATOSHI).add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE),
|
||||
EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
|
||||
@ -557,37 +719,42 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
} catch (ValueOutOfRangeException e) {}
|
||||
|
||||
// Verify that MIN_NONDUST_OUTPUT + MIN_TX_FEE is accepted
|
||||
clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()),
|
||||
clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()),
|
||||
Transaction.MIN_NONDUST_OUTPUT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
|
||||
// We'll have to pay REFERENCE_DEFAULT_MIN_TX_FEE twice (multisig+refund), and we'll end up getting back nearly nothing...
|
||||
clientState.initiate();
|
||||
assertEquals(clientState.getRefundTxFees(), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(2));
|
||||
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
|
||||
assertEquals(getInitialClientState(), clientState.getState());
|
||||
|
||||
// Now actually use a more useful CENT
|
||||
clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME);
|
||||
clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
|
||||
clientState.initiate();
|
||||
assertEquals(clientState.getRefundTxFees(), Coin.ZERO);
|
||||
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
|
||||
assertEquals(getInitialClientState(), clientState.getState());
|
||||
|
||||
// Send the refund tx from client to server and get back the signature.
|
||||
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientState.provideRefundSignature(refundSig, null);
|
||||
if (useRefunds()) {
|
||||
// Send the refund tx from client to server and get back the signature.
|
||||
Transaction refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientV1State().provideRefundSignature(refundSig, null);
|
||||
}
|
||||
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
|
||||
clientState.fakeSave();
|
||||
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
|
||||
|
||||
// Get the multisig contract
|
||||
Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize());
|
||||
Transaction multisigContract = new Transaction(params, clientState.getContract().bitcoinSerialize());
|
||||
assertEquals(PaymentChannelClientState.State.READY, clientState.getState());
|
||||
|
||||
// Provide the server with the multisig contract and simulate successful propagation/acceptance.
|
||||
serverState.provideMultiSigContract(multisigContract);
|
||||
if (!useRefunds()) {
|
||||
serverV2State().provideClientKey(clientState.myKey.getPubKey());
|
||||
}
|
||||
serverState.provideContract(multisigContract);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
|
||||
TxFuturePair pair = broadcasts.take();
|
||||
pair.future.set(pair.tx);
|
||||
@ -639,41 +806,63 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
Utils.setMockClock(); // Use mock clock
|
||||
final long EXPIRE_TIME = Utils.currentTimeMillis()/1000 + 60*60*24;
|
||||
|
||||
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
|
||||
serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(getInitialServerState(), serverState.getState());
|
||||
|
||||
clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) {
|
||||
@Override
|
||||
protected void editContractSendRequest(Wallet.SendRequest req) {
|
||||
req.coinSelector = wallet.getCoinSelector();
|
||||
}
|
||||
};
|
||||
switch (versionSelector) {
|
||||
case VERSION_1:
|
||||
clientState = new PaymentChannelV1ClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) {
|
||||
@Override
|
||||
protected void editContractSendRequest(Wallet.SendRequest req) {
|
||||
req.coinSelector = wallet.getCoinSelector();
|
||||
}
|
||||
};
|
||||
break;
|
||||
case VERSION_2_ALLOW_1:
|
||||
case VERSION_2:
|
||||
clientState = new PaymentChannelV2ClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), CENT, EXPIRE_TIME) {
|
||||
@Override
|
||||
protected void editContractSendRequest(Wallet.SendRequest req) {
|
||||
req.coinSelector = wallet.getCoinSelector();
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
|
||||
clientState.initiate();
|
||||
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
|
||||
assertEquals(getInitialClientState(), clientState.getState());
|
||||
|
||||
// Send the refund tx from client to server and get back the signature.
|
||||
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientState.provideRefundSignature(refundSig, null);
|
||||
if (useRefunds()) {
|
||||
// Send the refund tx from client to server and get back the signature.
|
||||
Transaction refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientV1State().provideRefundSignature(refundSig, null);
|
||||
}
|
||||
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
|
||||
clientState.fakeSave();
|
||||
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
|
||||
|
||||
// Validate the multisig contract looks right.
|
||||
Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize());
|
||||
assertEquals(PaymentChannelClientState.State.READY, clientState.getState());
|
||||
Transaction multisigContract = new Transaction(params, clientState.getContract().bitcoinSerialize());
|
||||
assertEquals(PaymentChannelV1ClientState.State.READY, clientState.getState());
|
||||
assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change.
|
||||
Script script = multisigContract.getOutput(0).getScriptPubKey();
|
||||
assertTrue(script.isSentToMultiSig());
|
||||
if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) {
|
||||
assertTrue(script.isSentToMultiSig());
|
||||
} else {
|
||||
assertTrue(script.isPayToScriptHash());
|
||||
}
|
||||
script = multisigContract.getOutput(1).getScriptPubKey();
|
||||
assertTrue(script.isSentToAddress());
|
||||
assertTrue(wallet.getPendingTransactions().contains(multisigContract));
|
||||
|
||||
// Provide the server with the multisig contract and simulate successful propagation/acceptance.
|
||||
serverState.provideMultiSigContract(multisigContract);
|
||||
if (!useRefunds()) {
|
||||
serverV2State().provideClientKey(clientState.myKey.getPubKey());
|
||||
}
|
||||
serverState.provideContract(multisigContract);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
|
||||
TxFuturePair pair = broadcasts.take();
|
||||
pair.future.set(pair.tx);
|
||||
@ -725,36 +914,48 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
Utils.setMockClock(); // Use mock clock
|
||||
final long EXPIRE_TIME = Utils.currentTimeSeconds() + 60*60*24;
|
||||
|
||||
serverState = new PaymentChannelServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_REFUND_TRANSACTION, serverState.getState());
|
||||
serverState = makeServerState(mockBroadcaster, serverWallet, serverKey, EXPIRE_TIME);
|
||||
assertEquals(getInitialServerState(), serverState.getState());
|
||||
|
||||
clientState = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
|
||||
clientState = makeClientState(wallet, myKey, ECKey.fromPublicOnly(serverKey.getPubKey()), halfCoin, EXPIRE_TIME);
|
||||
assertEquals(PaymentChannelClientState.State.NEW, clientState.getState());
|
||||
clientState.initiate();
|
||||
assertEquals(PaymentChannelClientState.State.INITIATED, clientState.getState());
|
||||
assertEquals(getInitialClientState(), clientState.getState());
|
||||
|
||||
// Send the refund tx from client to server and get back the signature.
|
||||
Transaction refund = new Transaction(params, clientState.getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
byte[] refundSig = serverState.provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientState.provideRefundSignature(refundSig, null);
|
||||
Transaction refund;
|
||||
if (useRefunds()) {
|
||||
refund = new Transaction(params, clientV1State().getIncompleteRefundTransaction().bitcoinSerialize());
|
||||
// Send the refund tx from client to server and get back the signature.
|
||||
byte[] refundSig = serverV1State().provideRefundTransaction(refund, myKey.getPubKey());
|
||||
assertEquals(PaymentChannelV1ServerState.State.WAITING_FOR_MULTISIG_CONTRACT, serverState.getState());
|
||||
// This verifies that the refund can spend the multi-sig output when run.
|
||||
clientV1State().provideRefundSignature(refundSig, null);
|
||||
} else {
|
||||
refund = clientV2State().getRefundTransaction();
|
||||
}
|
||||
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
|
||||
clientState.fakeSave();
|
||||
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
|
||||
|
||||
// Validate the multisig contract looks right.
|
||||
Transaction multisigContract = new Transaction(params, clientState.getMultisigContract().bitcoinSerialize());
|
||||
Transaction multisigContract = new Transaction(params, clientState.getContract().bitcoinSerialize());
|
||||
assertEquals(PaymentChannelClientState.State.READY, clientState.getState());
|
||||
assertEquals(2, multisigContract.getOutputs().size()); // One multi-sig, one change.
|
||||
Script script = multisigContract.getOutput(0).getScriptPubKey();
|
||||
assertTrue(script.isSentToMultiSig());
|
||||
if (versionSelector == PaymentChannelClient.VersionSelector.VERSION_1) {
|
||||
assertTrue(script.isSentToMultiSig());
|
||||
} else {
|
||||
assertTrue(script.isPayToScriptHash());
|
||||
}
|
||||
script = multisigContract.getOutput(1).getScriptPubKey();
|
||||
assertTrue(script.isSentToAddress());
|
||||
assertTrue(wallet.getPendingTransactions().contains(multisigContract));
|
||||
|
||||
// Provide the server with the multisig contract and simulate successful propagation/acceptance.
|
||||
serverState.provideMultiSigContract(multisigContract);
|
||||
if (!useRefunds()) {
|
||||
serverV2State().provideClientKey(clientState.myKey.getPubKey());
|
||||
}
|
||||
serverState.provideContract(multisigContract);
|
||||
assertEquals(PaymentChannelServerState.State.WAITING_FOR_MULTISIG_ACCEPTANCE, serverState.getState());
|
||||
final TxFuturePair pair = broadcasts.take();
|
||||
pair.future.set(pair.tx);
|
||||
@ -764,12 +965,12 @@ public class PaymentChannelStateTest extends TestWithWallet {
|
||||
assertEquals(2, wallet.getTransactions(false).size());
|
||||
Iterator<Transaction> walletTransactionIterator = wallet.getTransactions(false).iterator();
|
||||
Transaction clientWalletMultisigContract = walletTransactionIterator.next();
|
||||
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash()));
|
||||
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash()));
|
||||
if (!clientWalletMultisigContract.getHash().equals(multisigContract.getHash())) {
|
||||
clientWalletMultisigContract = walletTransactionIterator.next();
|
||||
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getCompletedRefundTransaction().getHash()));
|
||||
assertFalse(clientWalletMultisigContract.getHash().equals(clientState.getRefundTransaction().getHash()));
|
||||
} else
|
||||
assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getCompletedRefundTransaction().getHash()));
|
||||
assertFalse(walletTransactionIterator.next().getHash().equals(clientState.getRefundTransaction().getHash()));
|
||||
assertEquals(multisigContract.getHash(), clientWalletMultisigContract.getHash());
|
||||
assertFalse(clientWalletMultisigContract.getInput(0).getConnectedOutput().getSpentBy().getParentTransaction().getHash().equals(refund.getHash()));
|
||||
|
||||
|
@ -20,6 +20,7 @@ package org.bitcoinj.examples;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.kits.WalletAppKit;
|
||||
import org.bitcoinj.params.RegTestParams;
|
||||
import org.bitcoinj.protocols.channels.PaymentChannelClient;
|
||||
import org.bitcoinj.protocols.channels.PaymentChannelClientConnection;
|
||||
import org.bitcoinj.protocols.channels.StoredPaymentChannelClientStates;
|
||||
import org.bitcoinj.protocols.channels.ValueOutOfRangeException;
|
||||
@ -113,8 +114,9 @@ public class ExamplePaymentChannelClient {
|
||||
}
|
||||
|
||||
private void openAndSend(int timeoutSecs, InetSocketAddress server, String channelID, final int times) throws IOException, ValueOutOfRangeException, InterruptedException {
|
||||
// Use protocol version 1 for simplicity
|
||||
PaymentChannelClientConnection client = new PaymentChannelClientConnection(
|
||||
server, timeoutSecs, appKit.wallet(), myKey, channelSize, channelID);
|
||||
server, timeoutSecs, appKit.wallet(), myKey, channelSize, channelID, PaymentChannelClient.VersionSelector.VERSION_1);
|
||||
// Opening the channel requires talking to the server, so it's asynchronous.
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
Futures.addCallback(client.getChannelOpenFuture(), new FutureCallback<PaymentChannelClientConnection>() {
|
||||
|
@ -97,10 +97,10 @@ public class ExamplePaymentChannelServer implements PaymentChannelServerListener
|
||||
log.info(" with a maximum value of {}, expiring at UNIX timestamp {}.",
|
||||
// The channel's maximum value is the value of the multisig contract which locks in some
|
||||
// amount of money to the channel
|
||||
state.getMultisigContract().getOutput(0).getValue(),
|
||||
state.getContract().getOutput(0).getValue(),
|
||||
// The channel expires at some offset from when the client's refund transaction becomes
|
||||
// spendable.
|
||||
state.getRefundTransactionUnlockTime() + StoredPaymentChannelServerStates.CHANNEL_EXPIRE_OFFSET);
|
||||
state.getExpiryTime() + StoredPaymentChannelServerStates.CHANNEL_EXPIRE_OFFSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -724,7 +724,7 @@ public class WalletTool {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Wallet.SendRequest req = Wallet.SendRequest.toCLTVPaymentChannel(params, lockTime, refundKey, outputKey, value);
|
||||
Wallet.SendRequest req = Wallet.SendRequest.toCLTVPaymentChannel(params, BigInteger.valueOf(lockTime), refundKey, outputKey, value);
|
||||
if (req.tx.getOutputs().size() == 1 && req.tx.getOutput(0).getValue().equals(wallet.getBalance())) {
|
||||
log.info("Emptying out wallet, recipient may get less than what you expect");
|
||||
req.emptyWallet = true;
|
||||
|
Loading…
x
Reference in New Issue
Block a user