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