diff --git a/core/src/main/java/org/bitcoinj/net/ConnectionHandler.java b/core/src/main/java/org/bitcoinj/net/ConnectionHandler.java
index 57562042d..891126e5f 100644
--- a/core/src/main/java/org/bitcoinj/net/ConnectionHandler.java
+++ b/core/src/main/java/org/bitcoinj/net/ConnectionHandler.java
@@ -225,7 +225,8 @@ class ConnectionHandler implements MessageWriteTarget {
} catch (Exception e) {
// This can happen eg if the channel closes while the thread is about to get killed
// (ClosedByInterruptException), or if handler.parser.receiveBytes throws something
- log.error("Error handling SelectionKey: {}", Throwables.getRootCause(e).getMessage());
+ Throwable t = Throwables.getRootCause(e);
+ log.error("Error handling SelectionKey: {}", t.getMessage() != null ? t.getMessage() : t.getClass().getName());
handler.closeConnection();
}
}
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/ClientState.java b/core/src/main/java/org/bitcoinj/protocols/channels/ClientState.java
index 6cb63e909..85351aa12 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/ClientState.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/ClientState.java
@@ -1,5 +1,5 @@
// Generated by the protocol buffer compiler. DO NOT EDIT!
-// source: storedclientpaymentchannel.proto
+// source: core/src/storedclientpaymentchannel.proto
package org.bitcoinj.protocols.channels;
@@ -735,13 +735,31 @@ public final class ClientState {
*/
com.google.protobuf.ByteString getRefundTransaction();
+ // required bytes myPublicKey = 8;
+ /**
+ * required bytes myPublicKey = 8;
+ */
+ boolean hasMyPublicKey();
+ /**
+ * required bytes myPublicKey = 8;
+ */
+ com.google.protobuf.ByteString getMyPublicKey();
+
// required bytes myKey = 4;
/**
* required bytes myKey = 4;
+ *
+ *
+ * Deprecated, key is stored in the wallet, and found using myPublicKey;
+ *
*/
boolean hasMyKey();
/**
* required bytes myKey = 4;
+ *
+ *
+ * Deprecated, key is stored in the wallet, and found using myPublicKey;
+ *
*/
com.google.protobuf.ByteString getMyKey();
@@ -859,25 +877,30 @@ public final class ClientState {
break;
}
case 34: {
- bitField0_ |= 0x00000008;
+ bitField0_ |= 0x00000010;
myKey_ = input.readBytes();
break;
}
case 40: {
- bitField0_ |= 0x00000010;
+ bitField0_ |= 0x00000020;
valueToMe_ = input.readUInt64();
break;
}
case 48: {
- bitField0_ |= 0x00000020;
+ bitField0_ |= 0x00000040;
refundFees_ = input.readUInt64();
break;
}
case 58: {
- bitField0_ |= 0x00000040;
+ bitField0_ |= 0x00000080;
closeTransactionHash_ = input.readBytes();
break;
}
+ case 66: {
+ bitField0_ |= 0x00000008;
+ myPublicKey_ = input.readBytes();
+ break;
+ }
}
}
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
@@ -966,17 +989,41 @@ public final class ClientState {
return refundTransaction_;
}
+ // required bytes myPublicKey = 8;
+ public static final int MYPUBLICKEY_FIELD_NUMBER = 8;
+ private com.google.protobuf.ByteString myPublicKey_;
+ /**
+ * required bytes myPublicKey = 8;
+ */
+ public boolean hasMyPublicKey() {
+ return ((bitField0_ & 0x00000008) == 0x00000008);
+ }
+ /**
+ * required bytes myPublicKey = 8;
+ */
+ public com.google.protobuf.ByteString getMyPublicKey() {
+ return myPublicKey_;
+ }
+
// required bytes myKey = 4;
public static final int MYKEY_FIELD_NUMBER = 4;
private com.google.protobuf.ByteString myKey_;
/**
* required bytes myKey = 4;
+ *
+ *
+ * Deprecated, key is stored in the wallet, and found using myPublicKey;
+ *
*/
public boolean hasMyKey() {
- return ((bitField0_ & 0x00000008) == 0x00000008);
+ return ((bitField0_ & 0x00000010) == 0x00000010);
}
/**
* required bytes myKey = 4;
+ *
+ *
+ * Deprecated, key is stored in the wallet, and found using myPublicKey;
+ *
*/
public com.google.protobuf.ByteString getMyKey() {
return myKey_;
@@ -989,7 +1036,7 @@ public final class ClientState {
* required uint64 valueToMe = 5;
*/
public boolean hasValueToMe() {
- return ((bitField0_ & 0x00000010) == 0x00000010);
+ return ((bitField0_ & 0x00000020) == 0x00000020);
}
/**
* required uint64 valueToMe = 5;
@@ -1005,7 +1052,7 @@ public final class ClientState {
* required uint64 refundFees = 6;
*/
public boolean hasRefundFees() {
- return ((bitField0_ & 0x00000020) == 0x00000020);
+ return ((bitField0_ & 0x00000040) == 0x00000040);
}
/**
* required uint64 refundFees = 6;
@@ -1027,7 +1074,7 @@ public final class ClientState {
*
*/
public boolean hasCloseTransactionHash() {
- return ((bitField0_ & 0x00000040) == 0x00000040);
+ return ((bitField0_ & 0x00000080) == 0x00000080);
}
/**
* optional bytes closeTransactionHash = 7;
@@ -1046,6 +1093,7 @@ public final class ClientState {
id_ = com.google.protobuf.ByteString.EMPTY;
contractTransaction_ = com.google.protobuf.ByteString.EMPTY;
refundTransaction_ = com.google.protobuf.ByteString.EMPTY;
+ myPublicKey_ = com.google.protobuf.ByteString.EMPTY;
myKey_ = com.google.protobuf.ByteString.EMPTY;
valueToMe_ = 0L;
refundFees_ = 0L;
@@ -1068,6 +1116,10 @@ public final class ClientState {
memoizedIsInitialized = 0;
return false;
}
+ if (!hasMyPublicKey()) {
+ memoizedIsInitialized = 0;
+ return false;
+ }
if (!hasMyKey()) {
memoizedIsInitialized = 0;
return false;
@@ -1096,18 +1148,21 @@ public final class ClientState {
if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeBytes(3, refundTransaction_);
}
- if (((bitField0_ & 0x00000008) == 0x00000008)) {
+ if (((bitField0_ & 0x00000010) == 0x00000010)) {
output.writeBytes(4, myKey_);
}
- if (((bitField0_ & 0x00000010) == 0x00000010)) {
+ if (((bitField0_ & 0x00000020) == 0x00000020)) {
output.writeUInt64(5, valueToMe_);
}
- if (((bitField0_ & 0x00000020) == 0x00000020)) {
+ if (((bitField0_ & 0x00000040) == 0x00000040)) {
output.writeUInt64(6, refundFees_);
}
- if (((bitField0_ & 0x00000040) == 0x00000040)) {
+ if (((bitField0_ & 0x00000080) == 0x00000080)) {
output.writeBytes(7, closeTransactionHash_);
}
+ if (((bitField0_ & 0x00000008) == 0x00000008)) {
+ output.writeBytes(8, myPublicKey_);
+ }
getUnknownFields().writeTo(output);
}
@@ -1129,22 +1184,26 @@ public final class ClientState {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(3, refundTransaction_);
}
- if (((bitField0_ & 0x00000008) == 0x00000008)) {
+ if (((bitField0_ & 0x00000010) == 0x00000010)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(4, myKey_);
}
- if (((bitField0_ & 0x00000010) == 0x00000010)) {
+ if (((bitField0_ & 0x00000020) == 0x00000020)) {
size += com.google.protobuf.CodedOutputStream
.computeUInt64Size(5, valueToMe_);
}
- if (((bitField0_ & 0x00000020) == 0x00000020)) {
+ if (((bitField0_ & 0x00000040) == 0x00000040)) {
size += com.google.protobuf.CodedOutputStream
.computeUInt64Size(6, refundFees_);
}
- if (((bitField0_ & 0x00000040) == 0x00000040)) {
+ if (((bitField0_ & 0x00000080) == 0x00000080)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(7, closeTransactionHash_);
}
+ if (((bitField0_ & 0x00000008) == 0x00000008)) {
+ size += com.google.protobuf.CodedOutputStream
+ .computeBytesSize(8, myPublicKey_);
+ }
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
@@ -1272,14 +1331,16 @@ public final class ClientState {
bitField0_ = (bitField0_ & ~0x00000002);
refundTransaction_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000004);
- myKey_ = com.google.protobuf.ByteString.EMPTY;
+ myPublicKey_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000008);
- valueToMe_ = 0L;
+ myKey_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000010);
- refundFees_ = 0L;
+ valueToMe_ = 0L;
bitField0_ = (bitField0_ & ~0x00000020);
- closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
+ refundFees_ = 0L;
bitField0_ = (bitField0_ & ~0x00000040);
+ closeTransactionHash_ = com.google.protobuf.ByteString.EMPTY;
+ bitField0_ = (bitField0_ & ~0x00000080);
return this;
}
@@ -1323,18 +1384,22 @@ public final class ClientState {
if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
to_bitField0_ |= 0x00000008;
}
- result.myKey_ = myKey_;
+ result.myPublicKey_ = myPublicKey_;
if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
to_bitField0_ |= 0x00000010;
}
- result.valueToMe_ = valueToMe_;
+ result.myKey_ = myKey_;
if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
to_bitField0_ |= 0x00000020;
}
- result.refundFees_ = refundFees_;
+ result.valueToMe_ = valueToMe_;
if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
to_bitField0_ |= 0x00000040;
}
+ result.refundFees_ = refundFees_;
+ if (((from_bitField0_ & 0x00000080) == 0x00000080)) {
+ to_bitField0_ |= 0x00000080;
+ }
result.closeTransactionHash_ = closeTransactionHash_;
result.bitField0_ = to_bitField0_;
onBuilt();
@@ -1361,6 +1426,9 @@ public final class ClientState {
if (other.hasRefundTransaction()) {
setRefundTransaction(other.getRefundTransaction());
}
+ if (other.hasMyPublicKey()) {
+ setMyPublicKey(other.getMyPublicKey());
+ }
if (other.hasMyKey()) {
setMyKey(other.getMyKey());
}
@@ -1390,6 +1458,10 @@ public final class ClientState {
return false;
}
+ if (!hasMyPublicKey()) {
+
+ return false;
+ }
if (!hasMyKey()) {
return false;
@@ -1532,37 +1604,89 @@ public final class ClientState {
return this;
}
+ // required bytes myPublicKey = 8;
+ private com.google.protobuf.ByteString myPublicKey_ = com.google.protobuf.ByteString.EMPTY;
+ /**
+ * required bytes myPublicKey = 8;
+ */
+ public boolean hasMyPublicKey() {
+ return ((bitField0_ & 0x00000008) == 0x00000008);
+ }
+ /**
+ * required bytes myPublicKey = 8;
+ */
+ public com.google.protobuf.ByteString getMyPublicKey() {
+ return myPublicKey_;
+ }
+ /**
+ * required bytes myPublicKey = 8;
+ */
+ public Builder setMyPublicKey(com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ bitField0_ |= 0x00000008;
+ myPublicKey_ = value;
+ onChanged();
+ return this;
+ }
+ /**
+ * required bytes myPublicKey = 8;
+ */
+ public Builder clearMyPublicKey() {
+ bitField0_ = (bitField0_ & ~0x00000008);
+ myPublicKey_ = getDefaultInstance().getMyPublicKey();
+ onChanged();
+ return this;
+ }
+
// required bytes myKey = 4;
private com.google.protobuf.ByteString myKey_ = com.google.protobuf.ByteString.EMPTY;
/**
* required bytes myKey = 4;
+ *
+ *
+ * Deprecated, key is stored in the wallet, and found using myPublicKey;
+ *
*/
public boolean hasMyKey() {
- return ((bitField0_ & 0x00000008) == 0x00000008);
+ return ((bitField0_ & 0x00000010) == 0x00000010);
}
/**
* required bytes myKey = 4;
+ *
+ *
+ * Deprecated, key is stored in the wallet, and found using myPublicKey;
+ *
*/
public com.google.protobuf.ByteString getMyKey() {
return myKey_;
}
/**
* required bytes myKey = 4;
+ *
+ *
+ * Deprecated, key is stored in the wallet, and found using myPublicKey;
+ *
*/
public Builder setMyKey(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
- bitField0_ |= 0x00000008;
+ bitField0_ |= 0x00000010;
myKey_ = value;
onChanged();
return this;
}
/**
* required bytes myKey = 4;
+ *
+ *
+ * Deprecated, key is stored in the wallet, and found using myPublicKey;
+ *
*/
public Builder clearMyKey() {
- bitField0_ = (bitField0_ & ~0x00000008);
+ bitField0_ = (bitField0_ & ~0x00000010);
myKey_ = getDefaultInstance().getMyKey();
onChanged();
return this;
@@ -1574,7 +1698,7 @@ public final class ClientState {
* required uint64 valueToMe = 5;
*/
public boolean hasValueToMe() {
- return ((bitField0_ & 0x00000010) == 0x00000010);
+ return ((bitField0_ & 0x00000020) == 0x00000020);
}
/**
* required uint64 valueToMe = 5;
@@ -1586,7 +1710,7 @@ public final class ClientState {
* required uint64 valueToMe = 5;
*/
public Builder setValueToMe(long value) {
- bitField0_ |= 0x00000010;
+ bitField0_ |= 0x00000020;
valueToMe_ = value;
onChanged();
return this;
@@ -1595,7 +1719,7 @@ public final class ClientState {
* required uint64 valueToMe = 5;
*/
public Builder clearValueToMe() {
- bitField0_ = (bitField0_ & ~0x00000010);
+ bitField0_ = (bitField0_ & ~0x00000020);
valueToMe_ = 0L;
onChanged();
return this;
@@ -1607,7 +1731,7 @@ public final class ClientState {
* required uint64 refundFees = 6;
*/
public boolean hasRefundFees() {
- return ((bitField0_ & 0x00000020) == 0x00000020);
+ return ((bitField0_ & 0x00000040) == 0x00000040);
}
/**
* required uint64 refundFees = 6;
@@ -1619,7 +1743,7 @@ public final class ClientState {
* required uint64 refundFees = 6;
*/
public Builder setRefundFees(long value) {
- bitField0_ |= 0x00000020;
+ bitField0_ |= 0x00000040;
refundFees_ = value;
onChanged();
return this;
@@ -1628,7 +1752,7 @@ public final class ClientState {
* required uint64 refundFees = 6;
*/
public Builder clearRefundFees() {
- bitField0_ = (bitField0_ & ~0x00000020);
+ bitField0_ = (bitField0_ & ~0x00000040);
refundFees_ = 0L;
onChanged();
return this;
@@ -1646,7 +1770,7 @@ public final class ClientState {
*
*/
public boolean hasCloseTransactionHash() {
- return ((bitField0_ & 0x00000040) == 0x00000040);
+ return ((bitField0_ & 0x00000080) == 0x00000080);
}
/**
* optional bytes closeTransactionHash = 7;
@@ -1673,7 +1797,7 @@ public final class ClientState {
if (value == null) {
throw new NullPointerException();
}
- bitField0_ |= 0x00000040;
+ bitField0_ |= 0x00000080;
closeTransactionHash_ = value;
onChanged();
return this;
@@ -1688,7 +1812,7 @@ public final class ClientState {
*
*/
public Builder clearCloseTransactionHash() {
- bitField0_ = (bitField0_ & ~0x00000040);
+ bitField0_ = (bitField0_ & ~0x00000080);
closeTransactionHash_ = getDefaultInstance().getCloseTransactionHash();
onChanged();
return this;
@@ -1724,16 +1848,17 @@ public final class ClientState {
descriptor;
static {
java.lang.String[] descriptorData = {
- "\n storedclientpaymentchannel.proto\022\017paym" +
- "entchannels\"\\\n\033StoredClientPaymentChanne" +
- "ls\022=\n\010channels\030\001 \003(\0132+.paymentchannels.S" +
- "toredClientPaymentChannel\"\264\001\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\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\024closeTransactionHa" +
- "sh\030\007 \001(\014B.\n\037org.bitcoinj.protocols.chann" +
- "elsB\013ClientState"
+ "\n)core/src/storedclientpaymentchannel.pr" +
+ "oto\022\017paymentchannels\"\\\n\033StoredClientPaym" +
+ "entChannels\022=\n\010channels\030\001 \003(\0132+.paymentc" +
+ "hannels.StoredClientPaymentChannel\"\311\001\n\032S" +
+ "toredClientPaymentChannel\022\n\n\002id\030\001 \002(\014\022\033\n" +
+ "\023contractTransaction\030\002 \002(\014\022\031\n\021refundTran" +
+ "saction\030\003 \002(\014\022\023\n\013myPublicKey\030\010 \002(\014\022\r\n\005my" +
+ "Key\030\004 \002(\014\022\021\n\tvalueToMe\030\005 \002(\004\022\022\n\nrefundFe" +
+ "es\030\006 \002(\004\022\034\n\024closeTransactionHash\030\007 \001(\014B." +
+ "\n\037org.bitcoinj.protocols.channelsB\013Clien",
+ "tState"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
@@ -1751,7 +1876,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", "MyKey", "ValueToMe", "RefundFees", "CloseTransactionHash", });
+ new java.lang.String[] { "Id", "ContractTransaction", "RefundTransaction", "MyPublicKey", "MyKey", "ValueToMe", "RefundFees", "CloseTransactionHash", });
return null;
}
};
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/IPaymentChannelClient.java b/core/src/main/java/org/bitcoinj/protocols/channels/IPaymentChannelClient.java
index 328bd7564..692911f4d 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/IPaymentChannelClient.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/IPaymentChannelClient.java
@@ -17,11 +17,13 @@
package org.bitcoinj.protocols.channels;
import org.bitcoinj.core.Coin;
+import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import org.bitcoin.paymentchannel.Protos;
+import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
@@ -82,9 +84,12 @@ public interface IPaymentChannelClient {
* ({@link PaymentChannelClientConnection#state()}.getTotalValue())
* @throws IllegalStateException If the channel has been closed or is not yet open
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
+ * @throws ECKey.KeyIsEncryptedException If the keys are encrypted and no AES key has been provided,
* @return a future that completes when the server acknowledges receipt and acceptance of the payment.
*/
- ListenableFuture incrementPayment(Coin size, @Nullable ByteString info) throws ValueOutOfRangeException, IllegalStateException;
+ ListenableFuture incrementPayment(Coin size, @Nullable ByteString info,
+ @Nullable KeyParameter userKey)
+ throws ValueOutOfRangeException, IllegalStateException, ECKey.KeyIsEncryptedException;
/**
* Implements the connection between this client and the server, providing an interface which allows messages to be
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java
index 657fd13f4..f6096473f 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClient.java
@@ -28,9 +28,9 @@ import com.google.protobuf.ByteString;
import net.jcip.annotations.GuardedBy;
import org.bitcoin.paymentchannel.Protos;
import org.slf4j.LoggerFactory;
+import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
-import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.checkNotNull;
@@ -93,6 +93,9 @@ public class PaymentChannelClient implements IPaymentChannelClient {
private Coin missing;
+ // key to decrypt myKey, if it is encrypted, during setup.
+ private KeyParameter userKeySetup;
+
private final long timeWindow;
@GuardedBy("lock") private long minPayment;
@@ -127,7 +130,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
* the server)
*/
public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, ClientConnection conn) {
- this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, conn);
+ this(wallet,myKey,maxValue,serverId, DEFAULT_TIME_WINDOW, null, conn);
}
/**
@@ -146,10 +149,12 @@ public class PaymentChannelClient implements IPaymentChannelClient {
* @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)
*/
- public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, long timeWindow, ClientConnection conn) {
+ public PaymentChannelClient(Wallet wallet, ECKey myKey, Coin maxValue, Sha256Hash serverId, long timeWindow,
+ @Nullable KeyParameter userKeySetup, ClientConnection conn) {
this.wallet = checkNotNull(wallet);
this.myKey = checkNotNull(myKey);
this.maxValue = checkNotNull(maxValue);
@@ -157,6 +162,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
checkState(timeWindow >= 0);
this.timeWindow = timeWindow;
this.conn = checkNotNull(conn);
+ this.userKeySetup = userKeySetup;
}
/**
@@ -171,9 +177,13 @@ public class PaymentChannelClient implements IPaymentChannelClient {
@Nullable
@GuardedBy("lock")
- private CloseReason receiveInitiate(Protos.Initiate initiate, Coin contractValue, Protos.Error.Builder errorBuilder) throws VerificationException, InsufficientMoneyException {
+ private CloseReason receiveInitiate(Protos.Initiate initiate, Coin contractValue, Protos.Error.Builder errorBuilder)
+ throws VerificationException, InsufficientMoneyException, ECKey.KeyIsEncryptedException {
log.info("Got INITIATE message:\n{}", initiate.toString());
+ if (wallet.isEncrypted() && this.userKeySetup == null)
+ throw new ECKey.KeyIsEncryptedException();
+
final long expireTime = initiate.getExpireTimeSecs();
checkState( expireTime >= 0 && initiate.getMinAcceptedChannelSize() >= 0);
@@ -207,7 +217,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
throw new VerificationException("Server gave us a non-canonical public key, protocol error.");
state = new PaymentChannelClientState(wallet, myKey, ECKey.fromPublicOnly(pubKeyBytes), contractValue, expireTime);
try {
- state.initiate();
+ state.initiate(userKeySetup);
} catch (ValueOutOfRangeException e) {
log.error("Value out of range when trying to initiate", e);
errorBuilder.setCode(Protos.Error.ErrorCode.CHANNEL_VALUE_TOO_LARGE);
@@ -228,11 +238,11 @@ public class PaymentChannelClient implements IPaymentChannelClient {
}
@GuardedBy("lock")
- private void receiveRefund(Protos.TwoWayChannelMessage refundMsg) throws VerificationException {
+ private void receiveRefund(Protos.TwoWayChannelMessage refundMsg, @Nullable KeyParameter userKey) throws VerificationException {
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());
+ 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
@@ -244,7 +254,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
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));
+ PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(Coin.valueOf(minPayment), userKey);
Protos.UpdatePayment.Builder initialMsg = contractMsg.getInitialPaymentBuilder();
initialMsg.setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin()));
initialMsg.setClientChangeValue(state.getValueRefunded().value);
@@ -311,7 +321,9 @@ public class PaymentChannelClient implements IPaymentChannelClient {
log.error("Initiate failed with error: {}", errorBuilder.build().toString());
break;
case RETURN_REFUND:
- receiveRefund(msg);
+ receiveRefund(msg, userKeySetup);
+ // Key not used anymore
+ userKeySetup = null;
return;
case CHANNEL_OPEN:
receiveChannelOpen();
@@ -498,7 +510,7 @@ public class PaymentChannelClient implements IPaymentChannelClient {
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
*/
public ListenableFuture incrementPayment(Coin size) throws ValueOutOfRangeException, IllegalStateException {
- return incrementPayment(size, null);
+ return incrementPayment(size, null, null);
}
/**
@@ -510,22 +522,28 @@ public class PaymentChannelClient implements IPaymentChannelClient {
*
* @param size How many satoshis to increment the payment by (note: not the new total).
* @param info Information about this update, used to extend this protocol.
+ * @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted.
+ * The wallet KeyCrypter is assumed.
* @return a future that completes when the server acknowledges receipt and acceptance of the payment.
* @throws ValueOutOfRangeException If the size is negative or would pay more than this channel's total value
* ({@link PaymentChannelClientConnection#state()}.getTotalValue())
* @throws IllegalStateException If the channel has been closed or is not yet open
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
+ * @throws ECKey.KeyIsEncryptedException If the keys are encrypted and no AES key has been provided,
*/
@Override
- public ListenableFuture incrementPayment(Coin size, @Nullable ByteString info) throws ValueOutOfRangeException, IllegalStateException {
+ public ListenableFuture incrementPayment(Coin size, @Nullable ByteString info, @Nullable KeyParameter userKey)
+ throws ValueOutOfRangeException, IllegalStateException, ECKey.KeyIsEncryptedException {
lock.lock();
try {
if (state() == null || !connectionOpen || step != InitStep.CHANNEL_OPEN)
throw new IllegalStateException("Channel is not fully initialized/has already been closed");
if (increasePaymentFuture != null)
throw new IllegalStateException("Already incrementing paying, wait for previous payment to complete.");
+ if (wallet.isEncrypted() && userKey == null)
+ throw new ECKey.KeyIsEncryptedException();
- PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(size);
+ PaymentChannelClientState.IncrementedPayment payment = state().incrementPaymentBy(size, userKey);
Protos.UpdatePayment.Builder updatePaymentBuilder = Protos.UpdatePayment.newBuilder()
.setSignature(ByteString.copyFrom(payment.signature.encodeToBitcoin()))
.setClientChangeValue(state.getValueRefunded().value);
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java
index 769ef93bc..b39a25927 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientConnection.java
@@ -29,7 +29,9 @@ import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import org.bitcoin.paymentchannel.Protos;
+import org.spongycastle.crypto.params.KeyParameter;
+import javax.annotation.Nullable;
import java.io.IOException;
import java.net.InetSocketAddress;
@@ -45,14 +47,15 @@ public class PaymentChannelClientConnection {
/**
* 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}
+ * 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 already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
+ * Must be unencrypted. Must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
* @param myKey A freshly generated keypair used for the multisig contract and refund output.
* @param 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.
@@ -65,7 +68,8 @@ public class PaymentChannelClientConnection {
*/
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);
+ this(server, timeoutSeconds, wallet, myKey, maxValue, serverId,
+ PaymentChannelClient.DEFAULT_TIME_WINDOW, null);
}
/**
@@ -77,7 +81,8 @@ public class PaymentChannelClientConnection {
* @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 already have a {@link StoredPaymentChannelClientStates} object in its extensions set.
+ * 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.
@@ -85,16 +90,19 @@ public class PaymentChannelClientConnection {
* 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) throws IOException, ValueOutOfRangeException {
+ Coin maxValue, String serverId, final long timeWindow,
+ @Nullable KeyParameter userKeySetup)
+ 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.
channelClient = new PaymentChannelClient(wallet, myKey, maxValue, Sha256Hash.create(serverId.getBytes()), timeWindow,
- new PaymentChannelClient.ClientConnection() {
+ userKeySetup, new PaymentChannelClient.ClientConnection() {
@Override
public void sendToServer(Protos.TwoWayChannelMessage msg) {
wireParser.write(msg);
@@ -170,7 +178,7 @@ public class PaymentChannelClientConnection {
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
*/
public ListenableFuture incrementPayment(Coin size) throws ValueOutOfRangeException, IllegalStateException {
- return channelClient.incrementPayment(size, null);
+ return channelClient.incrementPayment(size, null, null);
}
/**
* Increments the total value which we pay the server.
@@ -182,8 +190,28 @@ public class PaymentChannelClientConnection {
* @throws IllegalStateException If the channel has been closed or is not yet open
* (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
*/
+ /*
public ListenableFuture incrementPayment(Coin size, ByteString info) throws ValueOutOfRangeException, IllegalStateException {
- return channelClient.incrementPayment(size, info);
+ return channelClient.incrementPayment(size, info, null);
+ }
+*/
+ /**
+ * Increments the total value which we pay the server.
+ *
+ * @param size How many satoshis to increment the payment by (note: not the new total).
+ * @param info Information about this payment increment, used to extend this protocol.
+ * @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 size is negative or would pay more than this channel's total value
+ * ({@link PaymentChannelClientConnection#state()}.getTotalValue())
+ * @throws IllegalStateException If the channel has been closed or is not yet open
+ * (see {@link PaymentChannelClientConnection#getChannelOpenFuture()} for the second)
+ */
+ public ListenableFuture incrementPayment(Coin size,
+ @Nullable ByteString info,
+ @Nullable KeyParameter userKey)
+ throws ValueOutOfRangeException, IllegalStateException {
+ return channelClient.incrementPayment(size, info, userKey);
}
/**
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java
index 9f1d72150..2f1e7ffb8 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelClientState.java
@@ -22,6 +22,7 @@ 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;
@@ -31,6 +32,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.annotation.Nullable;
import java.util.List;
import static com.google.common.base.Preconditions.*;
@@ -59,7 +61,7 @@ import static com.google.common.base.Preconditions.*;
* 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[])}.
+ * 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
* 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.
@@ -149,7 +151,7 @@ public class PaymentChannelClientState {
* @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 Utils#CENT} to avoid paying fees if you need to spend the refund transaction
+ * It is suggested you use at least {@link Coin#CENT} to avoid paying fees if you need to spend the refund transaction
* @param expiryTimeInSeconds At what point (UNIX timestamp +/- a few hours) the channel will expire
*
* @throws VerificationException If either myKey's pubkey or serverMultisigKey's pubkey are non-canonical (ie invalid)
@@ -235,7 +237,23 @@ 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() throws ValueOutOfRangeException, InsufficientMoneyException {
+ public void initiate() throws ValueOutOfRangeException, InsufficientMoneyException {
+ initiate(null);
+ }
+
+ /**
+ * 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)}.
+ * 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
+ */
+ 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
@@ -251,6 +269,7 @@ public class PaymentChannelClientState {
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;
@@ -291,7 +310,7 @@ 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)} to
+ * Once this step is done, you can use {@link PaymentChannelClientState#incrementPaymentBy(Coin, KeyParameter)} to
* start paying the server.
*/
public synchronized Transaction getMultisigContract() {
@@ -304,7 +323,7 @@ public class PaymentChannelClientState {
/**
* 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[])} with the result.
+ * {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)} with the result.
*/
public synchronized Transaction getIncompleteRefundTransaction() {
checkState(refundTx != null);
@@ -322,7 +341,8 @@ public class PaymentChannelClientState {
* transaction are automatically committed to wallet so that it can handle broadcasting the refund transaction at
* the appropriate time if necessary.
*/
- public synchronized void provideRefundSignature(byte[] theirSignature) throws VerificationException {
+ 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);
@@ -336,7 +356,8 @@ public class PaymentChannelClientState {
throw new RuntimeException(e); // Cannot happen: we built this ourselves.
}
TransactionSignature ourSignature =
- refundTx.calculateSignature(0, myKey, multisigScript, Transaction.SigHash.ALL, false);
+ 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);
@@ -390,7 +411,8 @@ public class PaymentChannelClientState {
* @throws ValueOutOfRangeException If size is negative or the channel does not have sufficient money in it to
* complete this payment.
*/
- public synchronized IncrementedPayment incrementPaymentBy(Coin size) throws ValueOutOfRangeException {
+ public synchronized IncrementedPayment incrementPaymentBy(Coin size, @Nullable KeyParameter userKey)
+ throws ValueOutOfRangeException {
checkState(state == State.READY);
checkNotExpired();
checkNotNull(size); // Validity of size will be checked by makeUnsignedChannelContract.
@@ -413,7 +435,7 @@ public class PaymentChannelClientState {
mode = Transaction.SigHash.NONE;
else
mode = Transaction.SigHash.SINGLE;
- TransactionSignature sig = tx.calculateSignature(0, myKey, multisigScript, mode, true);
+ TransactionSignature sig = tx.calculateSignature(0, myKey.maybeDecrypt(userKey), multisigScript, mode, true);
valueToMe = newValueToMe;
updateChannelInWallet();
IncrementedPayment payment = new IncrementedPayment();
@@ -506,7 +528,7 @@ public class PaymentChannelClientState {
/**
* Once the servers signature over the refund transaction has been received and provided using
- * {@link PaymentChannelClientState#provideRefundSignature(byte[])} then this
+ * {@link PaymentChannelClientState#provideRefundSignature(byte[], KeyParameter)} then this
* method can be called to receive the now valid and broadcastable refund transaction.
*/
public synchronized Transaction getCompletedRefundTransaction() {
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelClientStates.java b/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelClientStates.java
index 98ed1b2cf..1522274f1 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelClientStates.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/StoredPaymentChannelClientStates.java
@@ -264,13 +264,14 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
// First a few asserts to make sure things won't break
checkState(channel.valueToMe.signum() >= 0 && channel.valueToMe.compareTo(NetworkParameters.MAX_MONEY) < 0);
checkState(channel.refundFees.signum() >= 0 && channel.refundFees.compareTo(NetworkParameters.MAX_MONEY) < 0);
- checkNotNull(channel.myKey.getPrivKeyBytes());
+ checkNotNull(channel.myKey.getPubKey());
checkState(channel.refund.getConfidence().getSource() == TransactionConfidence.Source.SELF);
final ClientState.StoredClientPaymentChannel.Builder value = ClientState.StoredClientPaymentChannel.newBuilder()
.setId(ByteString.copyFrom(channel.id.getBytes()))
.setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize()))
.setRefundTransaction(ByteString.copyFrom(channel.refund.bitcoinSerialize()))
- .setMyKey(ByteString.copyFrom(channel.myKey.getPrivKeyBytes()))
+ .setMyKey(ByteString.copyFrom(new byte[0])) // Not used, but protobuf message requires
+ .setMyPublicKey(ByteString.copyFrom(channel.myKey.getPubKey()))
.setValueToMe(channel.valueToMe.value)
.setRefundFees(channel.refundFees.value);
if (channel.close != null)
@@ -294,10 +295,13 @@ public class StoredPaymentChannelClientStates implements WalletExtension {
for (ClientState.StoredClientPaymentChannel storedState : states.getChannelsList()) {
Transaction refundTransaction = new Transaction(params, storedState.getRefundTransaction().toByteArray());
refundTransaction.getConfidence().setSource(TransactionConfidence.Source.SELF);
+ ECKey myKey = (storedState.getMyKey().isEmpty()) ?
+ containingWallet.findKeyFromPubKey(storedState.getMyPublicKey().toByteArray()) :
+ ECKey.fromPrivate(storedState.getMyKey().toByteArray());
StoredClientChannel channel = new StoredClientChannel(new Sha256Hash(storedState.getId().toByteArray()),
new Transaction(params, storedState.getContractTransaction().toByteArray()),
refundTransaction,
- ECKey.fromPrivate(storedState.getMyKey().toByteArray()),
+ myKey,
Coin.valueOf(storedState.getValueToMe()),
Coin.valueOf(storedState.getRefundFees()), false);
if (storedState.hasCloseTransactionHash()) {
diff --git a/core/src/storedclientpaymentchannel.proto b/core/src/storedclientpaymentchannel.proto
index c3de819a8..5ce252878 100644
--- a/core/src/storedclientpaymentchannel.proto
+++ b/core/src/storedclientpaymentchannel.proto
@@ -39,6 +39,8 @@ message StoredClientPaymentChannel {
required bytes id = 1;
required bytes contractTransaction = 2;
required bytes refundTransaction = 3;
+ required bytes myPublicKey = 8;
+ // Deprecated, key is already stored in the wallet, and found using myPublicKey;
required bytes myKey = 4;
required uint64 valueToMe = 5;
required uint64 refundFees = 6;
diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java
index a0236e009..27d794ea2 100644
--- a/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java
+++ b/core/src/test/java/org/bitcoinj/protocols/channels/ChannelConnectionTest.java
@@ -29,6 +29,7 @@ import org.bitcoin.paymentchannel.Protos;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
@@ -116,6 +117,20 @@ public class ChannelConnectionTest extends TestWithWallet {
@Test
public void testSimpleChannel() throws Exception {
+ exectuteSimpleChannelTest(null);
+ }
+
+ @Test
+ public void testEncryptedClientWallet() throws Exception {
+ // Encrypt the client wallet
+ String mySecretPw = "MySecret";
+ wallet.encrypt(mySecretPw);
+
+ KeyParameter userKeySetup = wallet.getKeyCrypter().deriveKey(mySecretPw);
+ exectuteSimpleChannelTest(userKeySetup);
+ }
+
+ public void exectuteSimpleChannelTest(KeyParameter userKeySetup) throws Exception {
// Test with network code and without any issues. We'll broadcast two txns: multisig contract and settle transaction.
final SettableFuture> serverCloseFuture = SettableFuture.create();
final SettableFuture channelOpenFuture = SettableFuture.create();
@@ -147,7 +162,7 @@ public class ChannelConnectionTest extends TestWithWallet {
server.bindAndStart(4243);
PaymentChannelClientConnection client = new PaymentChannelClientConnection(
- new InetSocketAddress("localhost", 4243), 30, wallet, myKey, COIN, "");
+ new InetSocketAddress("localhost", 4243), 30, wallet, myKey, COIN, "", PaymentChannelClient.DEFAULT_TIME_WINDOW, userKeySetup);
// Wait for the multi-sig tx to be transmitted.
broadcastTxPause.release();
@@ -177,7 +192,7 @@ public class ChannelConnectionTest extends TestWithWallet {
q.take().assertPair(amount, null);
for (String info : new String[] {null, "one", "two"} ) {
final ByteString bytes = (info==null) ? null :ByteString.copyFromUtf8(info);
- final PaymentIncrementAck ack = client.incrementPayment(CENT, bytes).get();
+ final PaymentIncrementAck ack = client.incrementPayment(CENT, bytes, userKeySetup).get();
if (info != null) {
final ByteString ackInfo = ack.getInfo();
assertNotNull("Ack info is null", ackInfo);
diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java
index 9d374d180..ba48acf07 100644
--- a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java
+++ b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelClientTest.java
@@ -6,6 +6,7 @@ import org.easymock.Capture;
import org.easymock.EasyMock;
import org.junit.Before;
import org.junit.Test;
+import org.spongycastle.crypto.params.KeyParameter;
import java.util.HashMap;
@@ -49,7 +50,9 @@ public class PaymentChannelClientTest {
@Test
public void shouldSendTimeWindowInClientVersion() throws Exception {
long timeWindow = 4000;
- PaymentChannelClient dut = new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, timeWindow, connection);
+ KeyParameter userKey = null;
+ PaymentChannelClient dut =
+ new PaymentChannelClient(wallet, ecKey, maxValue, serverHash, timeWindow, userKey, connection);
connection.sendToServer(capture(clientVersionCapture));
EasyMock.expect(wallet.getExtensions()).andReturn(new HashMap());
replay(connection, wallet);
diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java
index 5c87f3571..ce15e16ef 100644
--- a/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java
+++ b/core/src/test/java/org/bitcoinj/protocols/channels/PaymentChannelStateTest.java
@@ -130,7 +130,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
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);
+ clientState.provideRefundSignature(refundSig, null);
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
@@ -169,7 +169,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
Coin size = halfCoin.divide(100);
Coin totalPayment = Coin.ZERO;
for (int i = 0; i < 4; i++) {
- byte[] signature = clientState.incrementPaymentBy(size).signature.encodeToBitcoin();
+ byte[] signature = clientState.incrementPaymentBy(size, null).signature.encodeToBitcoin();
totalPayment = totalPayment.add(size);
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
}
@@ -177,7 +177,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
// Now confirm the contract transaction and make sure payments still work
chain.add(makeSolvedTestBlock(blockStore.getChainHead().getHeader(), multisigContract));
- byte[] signature = clientState.incrementPaymentBy(size).signature.encodeToBitcoin();
+ byte[] signature = clientState.incrementPaymentBy(size, null).signature.encodeToBitcoin();
totalPayment = totalPayment.add(size);
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
@@ -248,7 +248,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
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);
+ clientState.provideRefundSignature(refundSig, null);
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
@@ -272,7 +272,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
// Pay a tiny bit
serverState.incrementPayment(CENT.divide(2).subtract(CENT.divide(10)),
- clientState.incrementPaymentBy(CENT.divide(10)).signature.encodeToBitcoin());
+ clientState.incrementPaymentBy(CENT.divide(10), null).signature.encodeToBitcoin());
// Advance time until our we get close enough to lock time that server should rebroadcast
Utils.rollMockClock(60*60*22);
@@ -319,7 +319,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
try {
// After its expired, we cant still increment payment
- clientState.incrementPaymentBy(CENT);
+ clientState.incrementPaymentBy(CENT, null);
fail();
} catch (IllegalStateException e) { }
}
@@ -378,7 +378,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
byte[] refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
refundSigCopy[refundSigCopy.length-1] = (byte) (Transaction.SigHash.NONE.ordinal() + 1);
try {
- clientState.provideRefundSignature(refundSigCopy);
+ clientState.provideRefundSignature(refundSigCopy, null);
fail();
} catch (VerificationException e) {
assertTrue(e.getMessage().contains("SIGHASH_NONE"));
@@ -387,7 +387,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
refundSigCopy[3] ^= 0x42; // Make the signature fail standard checks
try {
- clientState.provideRefundSignature(refundSigCopy);
+ clientState.provideRefundSignature(refundSigCopy, null);
fail();
} catch (VerificationException e) {
assertTrue(e.getMessage().contains("not canonical"));
@@ -396,7 +396,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
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);
+ clientState.provideRefundSignature(refundSigCopy, null);
fail();
} catch (VerificationException e) {
assertFalse(e.getMessage().contains("not canonical"));
@@ -404,13 +404,13 @@ public class PaymentChannelStateTest extends TestWithWallet {
refundSigCopy = Arrays.copyOf(refundSig, refundSig.length);
try { clientState.getCompletedRefundTransaction(); fail(); } catch (IllegalStateException e) {}
- clientState.provideRefundSignature(refundSigCopy);
- try { clientState.provideRefundSignature(refundSigCopy); 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());
- try { clientState.incrementPaymentBy(Coin.SATOSHI); fail(); } catch (IllegalStateException e) {}
+ try { clientState.incrementPaymentBy(Coin.SATOSHI, null); fail(); } catch (IllegalStateException e) {}
byte[] multisigContractSerialized = clientState.getMultisigContract().bitcoinSerialize();
@@ -456,11 +456,11 @@ public class PaymentChannelStateTest extends TestWithWallet {
Coin size = halfCoin.divide(100);
Coin totalPayment = Coin.ZERO;
try {
- clientState.incrementPaymentBy(COIN);
+ clientState.incrementPaymentBy(COIN, null);
fail();
} catch (ValueOutOfRangeException e) {}
- byte[] signature = clientState.incrementPaymentBy(size).signature.encodeToBitcoin();
+ byte[] signature = clientState.incrementPaymentBy(size, null).signature.encodeToBitcoin();
totalPayment = totalPayment.add(size);
byte[] signatureCopy = Arrays.copyOf(signature, signature.length);
@@ -491,7 +491,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
// Pay the rest (signed with SIGHASH_NONE|SIGHASH_ANYONECANPAY)
- byte[] signature2 = clientState.incrementPaymentBy(halfCoin.subtract(totalPayment)).signature.encodeToBitcoin();
+ byte[] signature2 = clientState.incrementPaymentBy(halfCoin.subtract(totalPayment), null).signature.encodeToBitcoin();
totalPayment = totalPayment.add(halfCoin.subtract(totalPayment));
assertEquals(totalPayment, halfCoin);
@@ -512,12 +512,12 @@ public class PaymentChannelStateTest extends TestWithWallet {
assertEquals(serverState.getBestValueToMe(), totalPayment);
try {
- clientState.incrementPaymentBy(Coin.SATOSHI.negate());
+ clientState.incrementPaymentBy(Coin.SATOSHI.negate(), null);
fail();
} catch (ValueOutOfRangeException e) {}
try {
- clientState.incrementPaymentBy(halfCoin.subtract(size).add(Coin.SATOSHI));
+ clientState.incrementPaymentBy(halfCoin.subtract(size).add(Coin.SATOSHI), null);
fail();
} catch (ValueOutOfRangeException e) {}
}
@@ -577,7 +577,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
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);
+ clientState.provideRefundSignature(refundSig, null);
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
@@ -597,7 +597,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
Coin totalPayment = Coin.ZERO;
// We can send as little as we want - its up to the server to get the fees right
- byte[] signature = clientState.incrementPaymentBy(Coin.SATOSHI).signature.encodeToBitcoin();
+ byte[] signature = clientState.incrementPaymentBy(Coin.SATOSHI, null).signature.encodeToBitcoin();
totalPayment = totalPayment.add(Coin.SATOSHI);
serverState.incrementPayment(CENT.subtract(totalPayment), signature);
@@ -610,7 +610,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
// We cannot send just under the total value - our refund would make it unspendable. So the client
// will correct it for us to be larger than the requested amount, to make the change output zero.
PaymentChannelClientState.IncrementedPayment payment =
- clientState.incrementPaymentBy(CENT.subtract(Transaction.MIN_NONDUST_OUTPUT));
+ clientState.incrementPaymentBy(CENT.subtract(Transaction.MIN_NONDUST_OUTPUT), null);
assertEquals(CENT.subtract(SATOSHI), payment.amount);
totalPayment = totalPayment.add(payment.amount);
@@ -657,7 +657,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
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);
+ clientState.provideRefundSignature(refundSig, null);
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
@@ -680,7 +680,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
assertEquals(PaymentChannelServerState.State.READY, serverState.getState());
// Both client and server are now in the ready state, split the channel in half
- byte[] signature = clientState.incrementPaymentBy(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(Coin.SATOSHI))
+ byte[] signature = clientState.incrementPaymentBy(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(Coin.SATOSHI), null)
.signature.encodeToBitcoin();
Coin totalRefund = CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(SATOSHI));
serverState.incrementPayment(totalRefund, signature);
@@ -705,7 +705,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
assertTrue(e.getMessage().contains("more in fees"));
}
- signature = clientState.incrementPaymentBy(SATOSHI.multiply(2)).signature.encodeToBitcoin();
+ signature = clientState.incrementPaymentBy(SATOSHI.multiply(2), null).signature.encodeToBitcoin();
totalRefund = totalRefund.subtract(SATOSHI.multiply(2));
serverState.incrementPayment(totalRefund, signature);
@@ -738,7 +738,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
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);
+ clientState.provideRefundSignature(refundSig, null);
assertEquals(PaymentChannelClientState.State.SAVE_STATE_IN_WALLET, clientState.getState());
clientState.fakeSave();
assertEquals(PaymentChannelClientState.State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, clientState.getState());
@@ -777,7 +777,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
Coin size = halfCoin.divide(100);
Coin totalPayment = Coin.ZERO;
for (int i = 0; i < 5; i++) {
- byte[] signature = clientState.incrementPaymentBy(size).signature.encodeToBitcoin();
+ byte[] signature = clientState.incrementPaymentBy(size, null).signature.encodeToBitcoin();
totalPayment = totalPayment.add(size);
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
}
@@ -794,7 +794,7 @@ public class PaymentChannelStateTest extends TestWithWallet {
// Now if we try to spend again the server will reject it since it saw a double-spend
try {
- byte[] signature = clientState.incrementPaymentBy(size).signature.encodeToBitcoin();
+ byte[] signature = clientState.incrementPaymentBy(size, null).signature.encodeToBitcoin();
totalPayment = totalPayment.add(size);
serverState.incrementPayment(halfCoin.subtract(totalPayment), signature);
fail();