3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-11 17:55:53 +00:00

PaymentChannelClient support encrypted wallets. Store will upgrade forward gracefully.

This commit is contained in:
Jarl Fransson 2014-11-09 20:21:48 +01:00 committed by ollekullberg
parent c017070398
commit df00b1e27e
11 changed files with 334 additions and 111 deletions

View File

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

View File

@ -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;
/**
* <code>required bytes myPublicKey = 8;</code>
*/
boolean hasMyPublicKey();
/**
* <code>required bytes myPublicKey = 8;</code>
*/
com.google.protobuf.ByteString getMyPublicKey();
// required bytes myKey = 4;
/**
* <code>required bytes myKey = 4;</code>
*
* <pre>
* Deprecated, key is stored in the wallet, and found using myPublicKey;
* </pre>
*/
boolean hasMyKey();
/**
* <code>required bytes myKey = 4;</code>
*
* <pre>
* Deprecated, key is stored in the wallet, and found using myPublicKey;
* </pre>
*/
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_;
/**
* <code>required bytes myPublicKey = 8;</code>
*/
public boolean hasMyPublicKey() {
return ((bitField0_ & 0x00000008) == 0x00000008);
}
/**
* <code>required bytes myPublicKey = 8;</code>
*/
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_;
/**
* <code>required bytes myKey = 4;</code>
*
* <pre>
* Deprecated, key is stored in the wallet, and found using myPublicKey;
* </pre>
*/
public boolean hasMyKey() {
return ((bitField0_ & 0x00000008) == 0x00000008);
return ((bitField0_ & 0x00000010) == 0x00000010);
}
/**
* <code>required bytes myKey = 4;</code>
*
* <pre>
* Deprecated, key is stored in the wallet, and found using myPublicKey;
* </pre>
*/
public com.google.protobuf.ByteString getMyKey() {
return myKey_;
@ -989,7 +1036,7 @@ public final class ClientState {
* <code>required uint64 valueToMe = 5;</code>
*/
public boolean hasValueToMe() {
return ((bitField0_ & 0x00000010) == 0x00000010);
return ((bitField0_ & 0x00000020) == 0x00000020);
}
/**
* <code>required uint64 valueToMe = 5;</code>
@ -1005,7 +1052,7 @@ public final class ClientState {
* <code>required uint64 refundFees = 6;</code>
*/
public boolean hasRefundFees() {
return ((bitField0_ & 0x00000020) == 0x00000020);
return ((bitField0_ & 0x00000040) == 0x00000040);
}
/**
* <code>required uint64 refundFees = 6;</code>
@ -1027,7 +1074,7 @@ public final class ClientState {
* </pre>
*/
public boolean hasCloseTransactionHash() {
return ((bitField0_ & 0x00000040) == 0x00000040);
return ((bitField0_ & 0x00000080) == 0x00000080);
}
/**
* <code>optional bytes closeTransactionHash = 7;</code>
@ -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;
/**
* <code>required bytes myPublicKey = 8;</code>
*/
public boolean hasMyPublicKey() {
return ((bitField0_ & 0x00000008) == 0x00000008);
}
/**
* <code>required bytes myPublicKey = 8;</code>
*/
public com.google.protobuf.ByteString getMyPublicKey() {
return myPublicKey_;
}
/**
* <code>required bytes myPublicKey = 8;</code>
*/
public Builder setMyPublicKey(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000008;
myPublicKey_ = value;
onChanged();
return this;
}
/**
* <code>required bytes myPublicKey = 8;</code>
*/
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;
/**
* <code>required bytes myKey = 4;</code>
*
* <pre>
* Deprecated, key is stored in the wallet, and found using myPublicKey;
* </pre>
*/
public boolean hasMyKey() {
return ((bitField0_ & 0x00000008) == 0x00000008);
return ((bitField0_ & 0x00000010) == 0x00000010);
}
/**
* <code>required bytes myKey = 4;</code>
*
* <pre>
* Deprecated, key is stored in the wallet, and found using myPublicKey;
* </pre>
*/
public com.google.protobuf.ByteString getMyKey() {
return myKey_;
}
/**
* <code>required bytes myKey = 4;</code>
*
* <pre>
* Deprecated, key is stored in the wallet, and found using myPublicKey;
* </pre>
*/
public Builder setMyKey(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000008;
bitField0_ |= 0x00000010;
myKey_ = value;
onChanged();
return this;
}
/**
* <code>required bytes myKey = 4;</code>
*
* <pre>
* Deprecated, key is stored in the wallet, and found using myPublicKey;
* </pre>
*/
public Builder clearMyKey() {
bitField0_ = (bitField0_ & ~0x00000008);
bitField0_ = (bitField0_ & ~0x00000010);
myKey_ = getDefaultInstance().getMyKey();
onChanged();
return this;
@ -1574,7 +1698,7 @@ public final class ClientState {
* <code>required uint64 valueToMe = 5;</code>
*/
public boolean hasValueToMe() {
return ((bitField0_ & 0x00000010) == 0x00000010);
return ((bitField0_ & 0x00000020) == 0x00000020);
}
/**
* <code>required uint64 valueToMe = 5;</code>
@ -1586,7 +1710,7 @@ public final class ClientState {
* <code>required uint64 valueToMe = 5;</code>
*/
public Builder setValueToMe(long value) {
bitField0_ |= 0x00000010;
bitField0_ |= 0x00000020;
valueToMe_ = value;
onChanged();
return this;
@ -1595,7 +1719,7 @@ public final class ClientState {
* <code>required uint64 valueToMe = 5;</code>
*/
public Builder clearValueToMe() {
bitField0_ = (bitField0_ & ~0x00000010);
bitField0_ = (bitField0_ & ~0x00000020);
valueToMe_ = 0L;
onChanged();
return this;
@ -1607,7 +1731,7 @@ public final class ClientState {
* <code>required uint64 refundFees = 6;</code>
*/
public boolean hasRefundFees() {
return ((bitField0_ & 0x00000020) == 0x00000020);
return ((bitField0_ & 0x00000040) == 0x00000040);
}
/**
* <code>required uint64 refundFees = 6;</code>
@ -1619,7 +1743,7 @@ public final class ClientState {
* <code>required uint64 refundFees = 6;</code>
*/
public Builder setRefundFees(long value) {
bitField0_ |= 0x00000020;
bitField0_ |= 0x00000040;
refundFees_ = value;
onChanged();
return this;
@ -1628,7 +1752,7 @@ public final class ClientState {
* <code>required uint64 refundFees = 6;</code>
*/
public Builder clearRefundFees() {
bitField0_ = (bitField0_ & ~0x00000020);
bitField0_ = (bitField0_ & ~0x00000040);
refundFees_ = 0L;
onChanged();
return this;
@ -1646,7 +1770,7 @@ public final class ClientState {
* </pre>
*/
public boolean hasCloseTransactionHash() {
return ((bitField0_ & 0x00000040) == 0x00000040);
return ((bitField0_ & 0x00000080) == 0x00000080);
}
/**
* <code>optional bytes closeTransactionHash = 7;</code>
@ -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 {
* </pre>
*/
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;
}
};

View File

@ -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<PaymentIncrementAck> incrementPayment(Coin size, @Nullable ByteString info) throws ValueOutOfRangeException, IllegalStateException;
ListenableFuture<PaymentIncrementAck> 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

View File

@ -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<PaymentIncrementAck> 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<PaymentIncrementAck> incrementPayment(Coin size, @Nullable ByteString info) throws ValueOutOfRangeException, IllegalStateException {
public ListenableFuture<PaymentIncrementAck> 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);

View File

@ -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<PaymentIncrementAck> 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<PaymentIncrementAck> 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<PaymentIncrementAck> incrementPayment(Coin size,
@Nullable ByteString info,
@Nullable KeyParameter userKey)
throws ValueOutOfRangeException, IllegalStateException {
return channelClient.incrementPayment(size, info, userKey);
}
/**

View File

@ -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.</p>
*/
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() {

View File

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

View File

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

View File

@ -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<ListenableFuture<PaymentChannelServerState>> serverCloseFuture = SettableFuture.create();
final SettableFuture<Sha256Hash> 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);

View File

@ -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<String, WalletExtension>());
replay(connection, wallet);

View File

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