3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-12 18:25:51 +00:00

Implemented version 2 of payment channels API

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

View File

@ -6006,11 +6006,17 @@ public final class Protos {
*
* <pre>
* 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

View File

@ -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.
}

View File

@ -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)

View File

@ -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);

View File

@ -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

View File

@ -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();
}

View File

@ -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();

View File

@ -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();
}

View File

@ -0,0 +1,297 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.wallet.AllowUnconfirmedCoinSelector;
import org.spongycastle.crypto.params.KeyParameter;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.List;
import static com.google.common.base.Preconditions.*;
/**
* Version 1 of the payment channel state machine - uses time locked multisig
* contracts.
*/
public class PaymentChannelV1ClientState extends PaymentChannelClientState {
private static final Logger log = LoggerFactory.getLogger(PaymentChannelV1ClientState.class);
// How much value (in satoshis) is locked up into the channel.
private final Coin totalValue;
// When the channel will automatically settle in favor of the client, if the server halts before protocol termination
// specified in terms of block timestamps (so it can off real time by a few hours).
private final long expiryTime;
// The refund is a time locked transaction that spends all the money of the channel back to the client.
private Transaction refundTx;
private Coin refundFees;
// The multi-sig contract locks the value of the channel up such that the agreement of both parties is required
// to spend it.
private Transaction multisigContract;
private Script multisigScript;
PaymentChannelV1ClientState(StoredClientChannel storedClientChannel, Wallet wallet) throws VerificationException {
super(storedClientChannel, wallet);
// The PaymentChannelClientConnection handles storedClientChannel.active and ensures we aren't resuming channels
this.multisigContract = checkNotNull(storedClientChannel.contract);
this.multisigScript = multisigContract.getOutput(0).getScriptPubKey();
this.refundTx = checkNotNull(storedClientChannel.refund);
this.refundFees = checkNotNull(storedClientChannel.refundFees);
this.expiryTime = refundTx.getLockTime();
this.totalValue = multisigContract.getOutput(0).getValue();
stateMachine.transition(State.READY);
initWalletListeners();
}
/**
* Creates a state object for a payment channel client. It is expected that you be ready to
* {@link PaymentChannelV1ClientState#initiate()} after construction (to avoid creating objects for channels which are
* not going to finish opening) and thus some parameters provided here are only used in
* {@link PaymentChannelV1ClientState#initiate()} to create the Multisig contract and refund transaction.
*
* @param wallet a wallet that contains at least the specified amount of value.
* @param myKey a freshly generated private key for this channel.
* @param serverMultisigKey a public key retrieved from the server used for the initial multisig contract
* @param value how many satoshis to put into this contract. If the channel reaches this limit, it must be closed.
* It is suggested you use at least {@link Coin#CENT} to avoid paying fees if you need to spend the refund transaction
* @param expiryTimeInSeconds At what point (UNIX timestamp +/- a few hours) the channel will expire
*
* @throws VerificationException If either myKey's pubkey or serverKey's pubkey are non-canonical (ie invalid)
*/
public PaymentChannelV1ClientState(Wallet wallet, ECKey myKey, ECKey serverMultisigKey,
Coin value, long expiryTimeInSeconds) throws VerificationException {
super(wallet, myKey, serverMultisigKey, value, expiryTimeInSeconds);
checkArgument(value.signum() > 0);
initWalletListeners();
this.totalValue = checkNotNull(value);
this.expiryTime = expiryTimeInSeconds;
stateMachine.transition(State.NEW);
}
@Override
protected Multimap<State, State> getStateTransitions() {
Multimap<State, State> result = MultimapBuilder.enumKeys(State.class).arrayListValues().build();
result.put(State.UNINITIALISED, State.NEW);
result.put(State.UNINITIALISED, State.READY);
result.put(State.NEW, State.INITIATED);
result.put(State.INITIATED, State.WAITING_FOR_SIGNED_REFUND);
result.put(State.WAITING_FOR_SIGNED_REFUND, State.SAVE_STATE_IN_WALLET);
result.put(State.SAVE_STATE_IN_WALLET, State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER);
result.put(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, State.READY);
result.put(State.READY, State.EXPIRED);
result.put(State.READY, State.CLOSED);
return result;
}
public int getMajorVersion() {
return 1;
}
/**
* Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate
* time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and
* {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by
* overriding {@link PaymentChannelV1ClientState#editContractSendRequest(org.bitcoinj.core.Wallet.SendRequest)}.
* By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low.
* @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted.
* The wallet KeyCrypter is assumed.
*
* @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network
* @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate
*/
@Override
public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException {
final NetworkParameters params = wallet.getParams();
Transaction template = new Transaction(params);
// We always place the client key before the server key because, if either side wants some privacy, they can
// use a fresh key for the the multisig contract and nowhere else
List<ECKey> keys = Lists.newArrayList(myKey, serverKey);
// There is also probably a change output, but we don't bother shuffling them as it's obvious from the
// format which one is the change. If we start obfuscating the change output better in future this may
// be worth revisiting.
TransactionOutput multisigOutput = template.addOutput(totalValue, ScriptBuilder.createMultiSigOutputScript(2, keys));
if (multisigOutput.getMinNonDustValue().compareTo(totalValue) > 0)
throw new ValueOutOfRangeException("totalValue too small to use");
Wallet.SendRequest req = Wallet.SendRequest.forTx(template);
req.coinSelector = AllowUnconfirmedCoinSelector.get();
editContractSendRequest(req);
req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable.
req.aesKey = userKey;
wallet.completeTx(req);
Coin multisigFee = req.tx.getFee();
multisigContract = req.tx;
// Build a refund transaction that protects us in the case of a bad server that's just trying to cause havoc
// by locking up peoples money (perhaps as a precursor to a ransom attempt). We time lock it so the server
// has an assurance that we cannot take back our money by claiming a refund before the channel closes - this
// relies on the fact that since Bitcoin 0.8 time locked transactions are non-final. This will need to change
// in future as it breaks the intended design of timelocking/tx replacement, but for now it simplifies this
// specific protocol somewhat.
refundTx = new Transaction(params);
refundTx.addInput(multisigOutput).setSequenceNumber(0); // Allow replacement when it's eventually reactivated.
refundTx.setLockTime(expiryTime);
if (totalValue.compareTo(Coin.CENT) < 0) {
// Must pay min fee.
final Coin valueAfterFee = totalValue.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
if (Transaction.MIN_NONDUST_OUTPUT.compareTo(valueAfterFee) > 0)
throw new ValueOutOfRangeException("totalValue too small to use");
refundTx.addOutput(valueAfterFee, myKey.toAddress(params));
refundFees = multisigFee.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
} else {
refundTx.addOutput(totalValue, myKey.toAddress(params));
refundFees = multisigFee;
}
refundTx.getConfidence().setSource(TransactionConfidence.Source.SELF);
log.info("initiated channel with multi-sig contract {}, refund {}", multisigContract.getHashAsString(),
refundTx.getHashAsString());
stateMachine.transition(State.INITIATED);
// Client should now call getIncompleteRefundTransaction() and send it to the server.
}
/**
* Returns the transaction that locks the money to the agreement of both parties. Do not mutate the result.
* Once this step is done, you can use {@link PaymentChannelClientState#incrementPaymentBy(Coin, KeyParameter)} to
* start paying the server.
*/
@Override
public synchronized Transaction getContract() {
checkState(multisigContract != null);
if (stateMachine.getState() == State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER) {
stateMachine.transition(State.READY);
}
return multisigContract;
}
@Override
protected synchronized Transaction getContractInternal() {
return multisigContract;
}
protected synchronized Script getContractScript() {
return multisigScript;
}
@Override
protected Script getSignedScript() {
return getContractScript();
}
/**
* Returns a partially signed (invalid) refund transaction that should be passed to the server. Once the server
* has checked it out and provided its own signature, call
* {@link PaymentChannelV1ClientState#provideRefundSignature(byte[], KeyParameter)} with the result.
*/
public synchronized Transaction getIncompleteRefundTransaction() {
checkState(refundTx != null);
if (stateMachine.getState() == State.INITIATED) {
stateMachine.transition(State.WAITING_FOR_SIGNED_REFUND);
}
return refundTx;
}
/**
* <p>When the servers signature for the refund transaction is received, call this to verify it and sign the
* complete refund ourselves.</p>
*
* <p>If this does not throw an exception, we are secure against the loss of funds and can safely provide the server
* with the multi-sig contract to lock in the agreement. In this case, both the multisig contract and the refund
* transaction are automatically committed to wallet so that it can handle broadcasting the refund transaction at
* the appropriate time if necessary.</p>
*/
public synchronized void provideRefundSignature(byte[] theirSignature, @Nullable KeyParameter userKey)
throws VerificationException {
checkNotNull(theirSignature);
stateMachine.checkState(State.WAITING_FOR_SIGNED_REFUND);
TransactionSignature theirSig = TransactionSignature.decodeFromBitcoin(theirSignature, true);
if (theirSig.sigHashMode() != Transaction.SigHash.NONE || !theirSig.anyoneCanPay())
throw new VerificationException("Refund signature was not SIGHASH_NONE|SIGHASH_ANYONECANPAY");
// Sign the refund transaction ourselves.
final TransactionOutput multisigContractOutput = multisigContract.getOutput(0);
try {
multisigScript = multisigContractOutput.getScriptPubKey();
} catch (ScriptException e) {
throw new RuntimeException(e); // Cannot happen: we built this ourselves.
}
TransactionSignature ourSignature =
refundTx.calculateSignature(0, myKey.maybeDecrypt(userKey),
multisigScript, Transaction.SigHash.ALL, false);
// Insert the signatures.
Script scriptSig = ScriptBuilder.createMultiSigInputScript(ourSignature, theirSig);
log.info("Refund scriptSig: {}", scriptSig);
log.info("Multi-sig contract scriptPubKey: {}", multisigScript);
TransactionInput refundInput = refundTx.getInput(0);
refundInput.setScriptSig(scriptSig);
refundInput.verify(multisigContractOutput);
stateMachine.transition(State.SAVE_STATE_IN_WALLET);
}
@Override
protected synchronized Coin getValueToMe() {
return valueToMe;
}
protected long getExpiryTime() {
return expiryTime;
}
@Override
@VisibleForTesting synchronized void doStoreChannelInWallet(Sha256Hash id) {
StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates)
wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
checkNotNull(channels, "You have not added the StoredPaymentChannelClientStates extension to the wallet.");
checkState(channels.getChannel(id, multisigContract.getHash()) == null);
storedChannel = new StoredClientChannel(getMajorVersion(), id, multisigContract, refundTx, myKey, serverKey, valueToMe, refundFees, 0, true);
channels.putChannel(storedChannel);
}
@Override
public synchronized Coin getRefundTxFees() {
checkState(getState().compareTo(State.NEW) > 0);
return refundFees;
}
@VisibleForTesting Transaction getRefundTransaction() {
return refundTx;
}
/**
* Once the servers signature over the refund transaction has been received and provided using
* {@link PaymentChannelV1ClientState#provideRefundSignature(byte[], KeyParameter)} then this
* method can be called to receive the now valid and broadcastable refund transaction.
*/
public synchronized Transaction getCompletedRefundTransaction() {
checkState(getState().compareTo(State.WAITING_FOR_SIGNED_REFUND) > 0);
return refundTx;
}
/**
* Gets the total value of this channel (ie the maximum payment possible)
*/
@Override
public Coin getTotalValue() {
return totalValue;
}
}

View File

@ -0,0 +1,279 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
import com.google.common.collect.*;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Locale;
import static com.google.common.base.Preconditions.*;
/**
* Version 1 of the payment channel server state object. Common functionality is
* present in the parent class.
*/
public class PaymentChannelV1ServerState extends PaymentChannelServerState {
private static final Logger log = LoggerFactory.getLogger(PaymentChannelV1ServerState.class);
// The total value locked into the multi-sig output and the value to us in the last signature the client provided
private Coin feePaidForPayment;
// The client key for the multi-sig contract
// We currently also use the serverKey for payouts, but this is not required
protected ECKey clientKey;
// The refund/change transaction output that goes back to the client
private TransactionOutput clientOutput;
private long refundTransactionUnlockTimeSecs;
PaymentChannelV1ServerState(StoredServerChannel storedServerChannel, Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException {
super(storedServerChannel, wallet, broadcaster);
synchronized (storedServerChannel) {
this.clientKey = ECKey.fromPublicOnly(getContractScript().getChunks().get(1).data);
this.clientOutput = checkNotNull(storedServerChannel.clientOutput);
this.refundTransactionUnlockTimeSecs = storedServerChannel.refundTransactionUnlockTimeSecs;
stateMachine.transition(State.READY);
}
}
/**
* Creates a new state object to track the server side of a payment channel.
*
* @param broadcaster The peer group which we will broadcast transactions to, this should have multiple peers
* @param wallet The wallet which will be used to complete transactions
* @param serverKey The private key which we use for our part of the multi-sig contract
* (this MUST be fresh and CANNOT be used elsewhere)
* @param minExpireTime The earliest time at which the client can claim the refund transaction (UNIX timestamp of block)
*/
public PaymentChannelV1ServerState(TransactionBroadcaster broadcaster, Wallet wallet, ECKey serverKey, long minExpireTime) {
super(broadcaster, wallet, serverKey, minExpireTime);
stateMachine.transition(State.WAITING_FOR_REFUND_TRANSACTION);
}
@Override
public Multimap<State, State> getStateTransitions() {
Multimap<State, State> result = MultimapBuilder.enumKeys(State.class).arrayListValues().build();
result.put(State.UNINITIALISED, State.READY);
result.put(State.UNINITIALISED, State.WAITING_FOR_REFUND_TRANSACTION);
result.put(State.WAITING_FOR_REFUND_TRANSACTION, State.WAITING_FOR_MULTISIG_CONTRACT);
result.put(State.WAITING_FOR_MULTISIG_CONTRACT, State.WAITING_FOR_MULTISIG_ACCEPTANCE);
result.put(State.WAITING_FOR_MULTISIG_ACCEPTANCE, State.READY);
result.put(State.READY, State.CLOSING);
result.put(State.CLOSING, State.CLOSED);
for (State state : State.values()) {
result.put(state, State.ERROR);
}
return result;
}
@Override
public int getMajorVersion() {
return 1;
}
@Override
public TransactionOutput getClientOutput() {
return clientOutput;
}
@Override
protected Script getSignedScript() {
return getContractScript();
}
/**
* Called when the client provides the refund transaction.
* The refund transaction must have one input from the multisig contract (that we don't have yet) and one output
* that the client creates to themselves. This object will later be modified when we start getting paid.
*
* @param refundTx The refund transaction, this object will be mutated when payment is incremented.
* @param clientMultiSigPubKey The client's pubkey which is required for the multisig output
* @return Our signature that makes the refund transaction valid
* @throws VerificationException If the transaction isnt valid or did not meet the requirements of a refund transaction.
*/
public synchronized byte[] provideRefundTransaction(Transaction refundTx, byte[] clientMultiSigPubKey) throws VerificationException {
checkNotNull(refundTx);
checkNotNull(clientMultiSigPubKey);
stateMachine.checkState(State.WAITING_FOR_REFUND_TRANSACTION);
log.info("Provided with refund transaction: {}", refundTx);
// Do a few very basic syntax sanity checks.
refundTx.verify();
// Verify that the refund transaction has a single input (that we can fill to sign the multisig output).
if (refundTx.getInputs().size() != 1)
throw new VerificationException("Refund transaction does not have exactly one input");
// Verify that the refund transaction has a time lock on it and a sequence number of zero.
if (refundTx.getInput(0).getSequenceNumber() != 0)
throw new VerificationException("Refund transaction's input's sequence number is non-0");
if (refundTx.getLockTime() < minExpireTime)
throw new VerificationException("Refund transaction has a lock time too soon");
// Verify the transaction has one output (we don't care about its contents, its up to the client)
// Note that because we sign with SIGHASH_NONE|SIGHASH_ANYOENCANPAY the client can later add more outputs and
// inputs, but we will need only one output later to create the paying transactions
if (refundTx.getOutputs().size() != 1)
throw new VerificationException("Refund transaction does not have exactly one output");
refundTransactionUnlockTimeSecs = refundTx.getLockTime();
// Sign the refund tx with the scriptPubKey and return the signature. We don't have the spending transaction
// so do the steps individually.
clientKey = ECKey.fromPublicOnly(clientMultiSigPubKey);
Script multisigPubKey = ScriptBuilder.createMultiSigOutputScript(2, ImmutableList.of(clientKey, serverKey));
// We are really only signing the fact that the transaction has a proper lock time and don't care about anything
// else, so we sign SIGHASH_NONE and SIGHASH_ANYONECANPAY.
TransactionSignature sig = refundTx.calculateSignature(0, serverKey, multisigPubKey, Transaction.SigHash.NONE, true);
log.info("Signed refund transaction.");
this.clientOutput = refundTx.getOutput(0);
stateMachine.transition(State.WAITING_FOR_MULTISIG_CONTRACT);
return sig.encodeToBitcoin();
}
protected Script createOutputScript() {
return ScriptBuilder.createMultiSigOutputScript(2, ImmutableList.<ECKey>of(clientKey, serverKey));
}
protected ECKey getClientKey() {
return clientKey;
}
// Signs the first input of the transaction which must spend the multisig contract.
private void signMultisigInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) {
TransactionSignature signature = tx.calculateSignature(0, serverKey, getContractScript(), hashType, anyoneCanPay);
byte[] mySig = signature.encodeToBitcoin();
Script scriptSig = ScriptBuilder.createMultiSigInputScriptBytes(ImmutableList.of(bestValueSignature, mySig));
tx.getInput(0).setScriptSig(scriptSig);
}
final SettableFuture<Transaction> closedFuture = SettableFuture.create();
/**
* <p>Closes this channel and broadcasts the highest value payment transaction on the network.</p>
*
* <p>This will set the state to {@link State#CLOSED} if the transaction is successfully broadcast on the network.
* If we fail to broadcast for some reason, the state is set to {@link State#ERROR}.</p>
*
* <p>If the current state is before {@link State#READY} (ie we have not finished initializing the channel), we
* simply set the state to {@link State#CLOSED} and let the client handle getting its refund transaction confirmed.
* </p>
*
* @return a future which completes when the provided multisig contract successfully broadcasts, or throws if the
* broadcast fails for some reason. Note that if the network simply rejects the transaction, this future
* will never complete, a timeout should be used.
* @throws InsufficientMoneyException If the payment tx would have cost more in fees to spend than it is worth.
*/
@Override
public synchronized ListenableFuture<Transaction> close() throws InsufficientMoneyException {
if (storedServerChannel != null) {
StoredServerChannel temp = storedServerChannel;
storedServerChannel = null;
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)
wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID);
channels.closeChannel(temp); // May call this method again for us (if it wasn't the original caller)
if (getState().compareTo(State.CLOSING) >= 0)
return closedFuture;
}
if (getState().ordinal() < State.READY.ordinal()) {
log.error("Attempt to settle channel in state " + getState());
stateMachine.transition(State.CLOSED);
closedFuture.set(null);
return closedFuture;
}
if (getState() != State.READY) {
// TODO: What is this codepath for?
log.warn("Failed attempt to settle a channel in state " + getState());
return closedFuture;
}
Transaction tx = null;
try {
Wallet.SendRequest req = makeUnsignedChannelContract(bestValueToMe);
tx = req.tx;
// Provide a throwaway signature so that completeTx won't complain out about unsigned inputs it doesn't
// know how to sign. Note that this signature does actually have to be valid, so we can't use a dummy
// signature to save time, because otherwise completeTx will try to re-sign it to make it valid and then
// die. We could probably add features to the SendRequest API to make this a bit more efficient.
signMultisigInput(tx, Transaction.SigHash.NONE, true);
// Let wallet handle adding additional inputs/fee as necessary.
req.shuffleOutputs = false;
req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG;
wallet.completeTx(req); // TODO: Fix things so shuffling is usable.
feePaidForPayment = req.tx.getFee();
log.info("Calculated fee is {}", feePaidForPayment);
if (feePaidForPayment.compareTo(bestValueToMe) > 0) {
final String msg = String.format(Locale.US, "Had to pay more in fees (%s) than the channel was worth (%s)",
feePaidForPayment, bestValueToMe);
throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg);
}
// Now really sign the multisig input.
signMultisigInput(tx, Transaction.SigHash.ALL, false);
// Some checks that shouldn't be necessary but it can't hurt to check.
tx.verify(); // Sanity check syntax.
for (TransactionInput input : tx.getInputs())
input.verify(); // Run scripts and ensure it is valid.
} catch (InsufficientMoneyException e) {
throw e; // Don't fall through.
} catch (Exception e) {
log.error("Could not verify self-built tx\nMULTISIG {}\nCLOSE {}", contract, tx != null ? tx : "");
throw new RuntimeException(e); // Should never happen.
}
stateMachine.transition(State.CLOSING);
log.info("Closing channel, broadcasting tx {}", tx);
// The act of broadcasting the transaction will add it to the wallet.
ListenableFuture<Transaction> future = broadcaster.broadcastTransaction(tx).future();
Futures.addCallback(future, new FutureCallback<Transaction>() {
@Override public void onSuccess(Transaction transaction) {
log.info("TX {} propagated, channel successfully closed.", transaction.getHash());
stateMachine.transition(State.CLOSED);
closedFuture.set(transaction);
}
@Override public void onFailure(Throwable throwable) {
log.error("Failed to settle channel, could not broadcast: {}", throwable);
stateMachine.transition(State.ERROR);
closedFuture.setException(throwable);
}
});
return closedFuture;
}
/**
* Gets the fee paid in the final payment transaction (only available if settle() did not throw an exception)
*/
@Override
public synchronized Coin getFeePaid() {
stateMachine.checkState(State.CLOSED, State.CLOSING);
return feePaidForPayment;
}
/**
* Gets the client's refund transaction which they can spend to get the entire channel value back if it reaches its
* lock time.
*/
public synchronized long getRefundTransactionUnlockTime() {
checkState(getState().compareTo(State.WAITING_FOR_MULTISIG_CONTRACT) > 0 && getState() != State.ERROR);
return refundTransactionUnlockTimeSecs;
}
}

View File

@ -0,0 +1,212 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.wallet.AllowUnconfirmedCoinSelector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
import java.math.BigInteger;
import java.util.List;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* Version 2 of the payment channel state machine - uses CLTV opcode transactions
* instead of multisig transactions.
*/
public class PaymentChannelV2ClientState extends PaymentChannelClientState {
private static final Logger log = LoggerFactory.getLogger(PaymentChannelV1ClientState.class);
// How much value (in satoshis) is locked up into the channel.
private final Coin totalValue;
// When the channel will automatically settle in favor of the client, if the server halts before protocol termination
// specified in terms of block timestamps (so it can off real time by a few hours).
private final long expiryTime;
// The refund is a time locked transaction that spends all the money of the channel back to the client.
// Unlike in V1 this refund isn't signed by the server - we only have to sign it ourselves.
@VisibleForTesting Transaction refundTx;
private Coin refundFees;
// The multi-sig contract locks the value of the channel up such that the agreement of both parties is required
// to spend it.
private Transaction contract;
PaymentChannelV2ClientState(StoredClientChannel storedClientChannel, Wallet wallet) throws VerificationException {
super(storedClientChannel, wallet);
// The PaymentChannelClientConnection handles storedClientChannel.active and ensures we aren't resuming channels
this.contract = checkNotNull(storedClientChannel.contract);
this.expiryTime = storedClientChannel.expiryTime;
this.totalValue = contract.getOutput(0).getValue();
this.valueToMe = checkNotNull(storedClientChannel.valueToMe);
this.refundTx = checkNotNull(storedClientChannel.refund);
this.refundFees = checkNotNull(storedClientChannel.refundFees);
stateMachine.transition(State.READY);
initWalletListeners();
}
public PaymentChannelV2ClientState(Wallet wallet, ECKey myKey, ECKey serverMultisigKey, Coin value, long expiryTimeInSeconds) throws VerificationException {
super(wallet, myKey, serverMultisigKey, value, expiryTimeInSeconds);
checkArgument(value.signum() > 0);
initWalletListeners();
this.valueToMe = this.totalValue = checkNotNull(value);
this.expiryTime = expiryTimeInSeconds;
stateMachine.transition(State.NEW);
}
@Override
protected Multimap<State, State> getStateTransitions() {
Multimap<State, State> result = MultimapBuilder.enumKeys(State.class).arrayListValues().build();
result.put(State.UNINITIALISED, State.NEW);
result.put(State.UNINITIALISED, State.READY);
result.put(State.NEW, State.SAVE_STATE_IN_WALLET);
result.put(State.SAVE_STATE_IN_WALLET, State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER);
result.put(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, State.READY);
result.put(State.READY, State.EXPIRED);
result.put(State.READY, State.CLOSED);
return result;
}
@Override
public int getMajorVersion() {
return 2;
}
@Override
public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException {
final NetworkParameters params = wallet.getParams();
Transaction template = new Transaction(params);
// There is also probably a change output, but we don't bother shuffling them as it's obvious from the
// format which one is the change. If we start obfuscating the change output better in future this may
// be worth revisiting.
Script redeemScript =
ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(expiryTime), myKey, serverKey);
TransactionOutput transactionOutput = template.addOutput(totalValue,
ScriptBuilder.createP2SHOutputScript(redeemScript));
if (transactionOutput.getMinNonDustValue().compareTo(totalValue) > 0)
throw new ValueOutOfRangeException("totalValue too small to use");
Wallet.SendRequest req = Wallet.SendRequest.forTx(template);
req.coinSelector = AllowUnconfirmedCoinSelector.get();
editContractSendRequest(req);
req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable.
req.aesKey = userKey;
wallet.completeTx(req);
Coin multisigFee = req.tx.getFee();
contract = req.tx;
// Build a refund transaction that protects us in the case of a bad server that's just trying to cause havoc
// by locking up peoples money (perhaps as a precursor to a ransom attempt). We time lock it so the server
// has an assurance that we cannot take back our money by claiming a refund before the channel closes - this
// relies on the fact that since Bitcoin 0.8 time locked transactions are non-final. This will need to change
// in future as it breaks the intended design of timelocking/tx replacement, but for now it simplifies this
// specific protocol somewhat.
refundTx = new Transaction(params);
refundTx.addInput(contract.getOutput(0)).setSequenceNumber(0); // Allow replacement when it's eventually reactivated.
refundTx.setLockTime(expiryTime);
if (totalValue.compareTo(Coin.CENT) < 0) {
// Must pay min fee.
final Coin valueAfterFee = totalValue.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
if (Transaction.MIN_NONDUST_OUTPUT.compareTo(valueAfterFee) > 0)
throw new ValueOutOfRangeException("totalValue too small to use");
refundTx.addOutput(valueAfterFee, myKey.toAddress(params));
refundFees = multisigFee.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
} else {
refundTx.addOutput(totalValue, myKey.toAddress(params));
refundFees = multisigFee;
}
TransactionSignature refundSignature =
refundTx.calculateSignature(0, myKey.maybeDecrypt(userKey),
getSignedScript(), Transaction.SigHash.ALL, false);
refundTx.getInput(0).setScriptSig(ScriptBuilder.createCLTVPaymentChannelP2SHRefund(refundSignature, redeemScript));
refundTx.getConfidence().setSource(TransactionConfidence.Source.SELF);
log.info("initiated channel with contract {}", contract.getHashAsString());
stateMachine.transition(State.SAVE_STATE_IN_WALLET);
// Client should now call getIncompleteRefundTransaction() and send it to the server.
}
@Override
protected synchronized Coin getValueToMe() {
return valueToMe;
}
protected long getExpiryTime() {
return expiryTime;
}
@Override
public synchronized Transaction getContract() {
checkState(contract != null);
if (stateMachine.getState() == State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER) {
stateMachine.transition(State.READY);
}
return contract;
}
@Override
protected synchronized Transaction getContractInternal() {
return contract;
}
protected synchronized Script getContractScript() {
return contract.getOutput(0).getScriptPubKey();
}
@Override
protected Script getSignedScript() {
return ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(expiryTime), myKey, serverKey);
}
@Override
public synchronized Coin getRefundTxFees() {
checkState(getState().compareTo(State.NEW) > 0);
return refundFees;
}
@VisibleForTesting Transaction getRefundTransaction() {
return refundTx;
}
@Override
@VisibleForTesting synchronized void doStoreChannelInWallet(Sha256Hash id) {
StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates)
wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
checkNotNull(channels, "You have not added the StoredPaymentChannelClientStates extension to the wallet.");
checkState(channels.getChannel(id, contract.getHash()) == null);
storedChannel = new StoredClientChannel(getMajorVersion(), id, contract, refundTx, myKey, serverKey, valueToMe, refundFees, expiryTime, true);
channels.putChannel(storedChannel);
}
@Override
public Coin getTotalValue() {
return totalValue;
}
}

View File

@ -0,0 +1,220 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Locale;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* Version 2 of the payment channel state machine - uses CLTV opcode transactions
* instead of multisig transactions.
*/
public class PaymentChannelV2ServerState extends PaymentChannelServerState {
private static final Logger log = LoggerFactory.getLogger(PaymentChannelV1ServerState.class);
// The total value locked into the CLTV output and the value to us in the last signature the client provided
private Coin feePaidForPayment;
// The client key for the multi-sig contract
// We currently also use the serverKey for payouts, but this is not required
protected ECKey clientKey;
PaymentChannelV2ServerState(StoredServerChannel storedServerChannel, Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException {
super(storedServerChannel, wallet, broadcaster);
synchronized (storedServerChannel) {
this.clientKey = storedServerChannel.clientKey;
stateMachine.transition(State.READY);
}
}
public PaymentChannelV2ServerState(TransactionBroadcaster broadcaster, Wallet wallet, ECKey serverKey, long minExpireTime) {
super(broadcaster, wallet, serverKey, minExpireTime);
stateMachine.transition(State.WAITING_FOR_MULTISIG_CONTRACT);
}
@Override
public Multimap<State, State> getStateTransitions() {
Multimap<State, State> result = MultimapBuilder.enumKeys(State.class).arrayListValues().build();
result.put(State.UNINITIALISED, State.READY);
result.put(State.UNINITIALISED, State.WAITING_FOR_MULTISIG_CONTRACT);
result.put(State.WAITING_FOR_MULTISIG_CONTRACT, State.WAITING_FOR_MULTISIG_ACCEPTANCE);
result.put(State.WAITING_FOR_MULTISIG_ACCEPTANCE, State.READY);
result.put(State.READY, State.CLOSING);
result.put(State.CLOSING, State.CLOSED);
for (State state : State.values()) {
result.put(state, State.ERROR);
}
return result;
}
@Override
public int getMajorVersion() {
return 2;
}
@Override
public TransactionOutput getClientOutput() {
return null;
}
public void provideClientKey(byte[] clientKey) {
this.clientKey = ECKey.fromPublicOnly(clientKey);
}
@Override
public synchronized Coin getFeePaid() {
stateMachine.checkState(State.CLOSED, State.CLOSING);
return feePaidForPayment;
}
@Override
protected Script getSignedScript() {
return createP2SHRedeemScript();
}
@Override
protected void verifyContract(final Transaction contract) {
super.verifyContract(contract);
// Check contract matches P2SH hash
byte[] expected = getContractScript().getPubKeyHash();
byte[] actual = Utils.sha256hash160(createP2SHRedeemScript().getProgram());
if (!Arrays.equals(actual, expected)) {
throw new VerificationException(
"P2SH hash didn't match required contract - contract should be a CLTV micropayment channel to client and server in that order.");
}
}
/**
* Creates a P2SH script outputting to the client and server pubkeys
* @return
*/
@Override
protected Script createOutputScript() {
return ScriptBuilder.createP2SHOutputScript(createP2SHRedeemScript());
}
private Script createP2SHRedeemScript() {
return ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(getExpiryTime()), clientKey, serverKey);
}
protected ECKey getClientKey() {
return clientKey;
}
// Signs the first input of the transaction which must spend the multisig contract.
private void signP2SHInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) {
TransactionSignature signature = tx.calculateSignature(0, serverKey, createP2SHRedeemScript(), hashType, anyoneCanPay);
byte[] mySig = signature.encodeToBitcoin();
Script scriptSig = ScriptBuilder.createCLTVPaymentChannelP2SHInput(bestValueSignature, mySig, createP2SHRedeemScript());
tx.getInput(0).setScriptSig(scriptSig);
}
final SettableFuture<Transaction> closedFuture = SettableFuture.create();
@Override
public synchronized ListenableFuture<Transaction> close() throws InsufficientMoneyException {
if (storedServerChannel != null) {
StoredServerChannel temp = storedServerChannel;
storedServerChannel = null;
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)
wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID);
channels.closeChannel(temp); // May call this method again for us (if it wasn't the original caller)
if (getState().compareTo(State.CLOSING) >= 0)
return closedFuture;
}
if (getState().ordinal() < State.READY.ordinal()) {
log.error("Attempt to settle channel in state " + getState());
stateMachine.transition(State.CLOSED);
closedFuture.set(null);
return closedFuture;
}
if (getState() != State.READY) {
// TODO: What is this codepath for?
log.warn("Failed attempt to settle a channel in state " + getState());
return closedFuture;
}
Transaction tx = null;
try {
Wallet.SendRequest req = makeUnsignedChannelContract(bestValueToMe);
tx = req.tx;
// Provide a throwaway signature so that completeTx won't complain out about unsigned inputs it doesn't
// know how to sign. Note that this signature does actually have to be valid, so we can't use a dummy
// signature to save time, because otherwise completeTx will try to re-sign it to make it valid and then
// die. We could probably add features to the SendRequest API to make this a bit more efficient.
signP2SHInput(tx, Transaction.SigHash.NONE, true);
// Let wallet handle adding additional inputs/fee as necessary.
req.shuffleOutputs = false;
req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG;
wallet.completeTx(req); // TODO: Fix things so shuffling is usable.
feePaidForPayment = req.tx.getFee();
log.info("Calculated fee is {}", feePaidForPayment);
if (feePaidForPayment.compareTo(bestValueToMe) > 0) {
final String msg = String.format(Locale.US, "Had to pay more in fees (%s) than the channel was worth (%s)",
feePaidForPayment, bestValueToMe);
throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg);
}
// Now really sign the multisig input.
signP2SHInput(tx, Transaction.SigHash.ALL, false);
// Some checks that shouldn't be necessary but it can't hurt to check.
tx.verify(); // Sanity check syntax.
for (TransactionInput input : tx.getInputs())
input.verify(); // Run scripts and ensure it is valid.
} catch (InsufficientMoneyException e) {
throw e; // Don't fall through.
} catch (Exception e) {
log.error("Could not verify self-built tx\nMULTISIG {}\nCLOSE {}", contract, tx != null ? tx : "");
throw new RuntimeException(e); // Should never happen.
}
stateMachine.transition(State.CLOSING);
log.info("Closing channel, broadcasting tx {}", tx);
// The act of broadcasting the transaction will add it to the wallet.
ListenableFuture<Transaction> future = broadcaster.broadcastTransaction(tx).future();
Futures.addCallback(future, new FutureCallback<Transaction>() {
@Override public void onSuccess(Transaction transaction) {
log.info("TX {} propagated, channel successfully closed.", transaction.getHash());
stateMachine.transition(State.CLOSED);
closedFuture.set(transaction);
}
@Override public void onFailure(Throwable throwable) {
log.error("Failed to settle channel, could not broadcast: {}", throwable);
stateMachine.transition(State.ERROR);
closedFuture.setException(throwable);
}
});
return closedFuture;
}
}

View File

@ -745,11 +745,11 @@ public final class ServerState {
com.google.protobuf.ByteString getContractTransaction();
/**
* <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)

View File

@ -0,0 +1,87 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import java.util.Locale;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A class representing a state machine, with limited transitions between states.
* @param <State> An enum of states to use
*/
public class StateMachine<State extends Enum<State>> {
private State currentState;
private final Multimap<State, State> transitions;
public StateMachine(State startState, Multimap<State, State> transitions) {
currentState = checkNotNull(startState);
this.transitions = checkNotNull(transitions);
}
/**
* Checks that the machine is in the given state. Throws if it isn't.
* @param requiredState
*/
public synchronized void checkState(State requiredState) throws IllegalStateException {
if (requiredState != currentState) {
throw new IllegalStateException(String.format(Locale.US,
"Expected state %s, but in state %s", requiredState, currentState));
}
}
/**
* Checks that the machine is in one of the given states. Throws if it isn't.
* @param requiredStates
*/
public synchronized void checkState(State... requiredStates) throws IllegalStateException {
for (State requiredState : requiredStates) {
if (requiredState.equals(currentState)) {
return;
}
}
throw new IllegalStateException(String.format(Locale.US,
"Expected states %s, but in state %s", Lists.newArrayList(requiredStates), currentState));
}
/**
* Transitions to a new state, provided that the required transition exists
* @param newState
* @throws IllegalStateException If no state transition exists from oldState to newState
*/
public synchronized void transition(State newState) throws IllegalStateException {
if (transitions.containsEntry(currentState, newState)) {
currentState = newState;
} else {
throw new IllegalStateException(String.format(Locale.US,
"Attempted invalid transition from %s to %s", currentState, newState));
}
}
public synchronized State getState() {
return currentState;
}
@Override
public String toString() {
return new StringBuilder().append('[').append(getState()).append(']').toString();
}
}

View File

@ -17,6 +17,8 @@
package org.bitcoinj.protocols.channels;
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);

View File

@ -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);

View File

@ -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 + " "));
}

View File

@ -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();
}

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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));

View File

@ -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 );
}

View File

@ -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();
}

View File

@ -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()));

View File

@ -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>() {

View File

@ -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

View File

@ -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;