From 01cff428d3bbc454b64665ea7cc73588209b37a0 Mon Sep 17 00:00:00 2001
From: Will Shackleton
Date: Fri, 8 Apr 2016 15:12:12 +0100
Subject: [PATCH] Payment channels: Add KeyParameter support to
PaymentChannelServer
I added KeyParameter support to PaymentChannelServer. This results in an
API change---`PaymentChannelServer.ServerConnection` now has a new
method `getUserKey()`
---
.../channels/PaymentChannelServer.java | 23 ++++++++++++++++++-
.../PaymentChannelServerListener.java | 7 ++++++
.../channels/PaymentChannelServerState.java | 16 ++++++++++++-
.../channels/PaymentChannelV1ServerState.java | 14 +++++++----
.../channels/PaymentChannelV2ServerState.java | 13 +++++++----
.../protocols/channels/ChannelTestUtils.java | 7 ++++++
6 files changed, 68 insertions(+), 12 deletions(-)
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java
index f53daf22..8a4c6e22 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServer.java
@@ -17,6 +17,7 @@
package org.bitcoinj.protocols.channels;
import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.AsyncFunction;
import org.bitcoinj.core.*;
import org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason;
import org.bitcoinj.utils.Threading;
@@ -29,6 +30,7 @@ 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.Map;
@@ -117,6 +119,13 @@ public class PaymentChannelServer {
*/
@Nullable
ListenableFuture paymentIncrease(Coin by, Coin to, @Nullable ByteString info);
+
+ /**
+ * Called when a channel is being closed and must be signed, possibly with an encrypted key.
+ * @return A future for the (nullable) KeyParameter for the ECKey, or null
if no key is required.
+ */
+ @Nullable
+ ListenableFuture getUserKey();
}
private final ServerConnection conn;
@@ -520,7 +529,19 @@ public class PaymentChannelServer {
// close() on us here below via the stored channel state.
// TODO: Strongly separate the lifecycle of the payment channel from the TCP connection in these classes.
channelSettling = true;
- Futures.addCallback(state.close(), new FutureCallback() {
+ ListenableFuture keyFuture = conn.getUserKey();
+ ListenableFuture result;
+ if (keyFuture != null) {
+ result = Futures.transform(conn.getUserKey(), new AsyncFunction() {
+ @Override
+ public ListenableFuture apply(KeyParameter userKey) throws Exception {
+ return state.close(userKey);
+ }
+ });
+ } else {
+ result = state.close();
+ }
+ Futures.addCallback(result, new FutureCallback() {
@Override
public void onSuccess(Transaction result) {
// Send the successfully accepted transaction back to the client.
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerListener.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerListener.java
index 2881c046..5c0943e4 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerListener.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerListener.java
@@ -28,6 +28,7 @@ import org.bitcoinj.wallet.Wallet;
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;
@@ -88,6 +89,12 @@ public class PaymentChannelServerListener {
@Override public ListenableFuture paymentIncrease(Coin by, Coin to, @Nullable ByteString info) {
return eventHandler.paymentIncrease(by, to, info);
}
+
+ @Nullable
+ @Override
+ public ListenableFuture getUserKey() {
+ return null;
+ }
});
protobufHandlerListener = new ProtobufConnection.Listener() {
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerState.java
index a5c9a785..f69783a0 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerState.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelServerState.java
@@ -30,6 +30,7 @@ import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
@@ -300,7 +301,20 @@ public abstract class PaymentChannelServerState {
* will never complete, a timeout should be used.
* @throws InsufficientMoneyException If the payment tx would have cost more in fees to spend than it is worth.
*/
- public abstract ListenableFuture close() throws InsufficientMoneyException;
+ public ListenableFuture close() throws InsufficientMoneyException {
+ return close(null);
+ }
+
+ /**
+ * Closes this channel and broadcasts the highest value payment transaction on the network.
+ *
+ * @param userKey The AES key to use for decryption of the private key. If null then no decryption is required.
+ * @return a future which completes when the provided multisig contract successfully broadcasts, or throws if the
+ * broadcast fails for some reason. Note that if the network simply rejects the transaction, this future
+ * will never complete, a timeout should be used.
+ * @throws InsufficientMoneyException If the payment tx would have cost more in fees to spend than it is worth.
+ */
+ public abstract ListenableFuture close(@Nullable KeyParameter userKey) throws InsufficientMoneyException;
/**
* Gets the highest payment to ourselves (which we will receive on settle(), not including fees)
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ServerState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ServerState.java
index 3bf1df3f..448f0b35 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ServerState.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV1ServerState.java
@@ -30,7 +30,9 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.spongycastle.crypto.params.KeyParameter;
+import javax.annotation.Nullable;
import java.util.Locale;
import static com.google.common.base.Preconditions.*;
@@ -163,8 +165,9 @@ public class PaymentChannelV1ServerState extends PaymentChannelServerState {
}
// Signs the first input of the transaction which must spend the multisig contract.
- private void signMultisigInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) {
- TransactionSignature signature = tx.calculateSignature(0, serverKey, getContractScript(), hashType, anyoneCanPay);
+ private void signMultisigInput(Transaction tx, Transaction.SigHash hashType,
+ boolean anyoneCanPay, @Nullable KeyParameter userKey) {
+ TransactionSignature signature = tx.calculateSignature(0, serverKey, userKey, getContractScript(), hashType, anyoneCanPay);
byte[] mySig = signature.encodeToBitcoin();
Script scriptSig = ScriptBuilder.createMultiSigInputScriptBytes(ImmutableList.of(bestValueSignature, mySig));
tx.getInput(0).setScriptSig(scriptSig);
@@ -181,13 +184,14 @@ public class PaymentChannelV1ServerState extends PaymentChannelServerState {
* simply set the state to {@link State#CLOSED} and let the client handle getting its refund transaction confirmed.
*
*
+ * @param userKey The AES key to use for decryption of the private key. If null then no decryption is required.
* @return a future which completes when the provided multisig contract successfully broadcasts, or throws if the
* broadcast fails for some reason. Note that if the network simply rejects the transaction, this future
* will never complete, a timeout should be used.
* @throws InsufficientMoneyException If the payment tx would have cost more in fees to spend than it is worth.
*/
@Override
- public synchronized ListenableFuture close() throws InsufficientMoneyException {
+ public synchronized ListenableFuture close(@Nullable KeyParameter userKey) throws InsufficientMoneyException {
if (storedServerChannel != null) {
StoredServerChannel temp = storedServerChannel;
storedServerChannel = null;
@@ -217,7 +221,7 @@ public class PaymentChannelV1ServerState extends PaymentChannelServerState {
// know how to sign. Note that this signature does actually have to be valid, so we can't use a dummy
// signature to save time, because otherwise completeTx will try to re-sign it to make it valid and then
// die. We could probably add features to the SendRequest API to make this a bit more efficient.
- signMultisigInput(tx, Transaction.SigHash.NONE, true);
+ signMultisigInput(tx, Transaction.SigHash.NONE, true, userKey);
// Let wallet handle adding additional inputs/fee as necessary.
req.shuffleOutputs = false;
req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG;
@@ -230,7 +234,7 @@ public class PaymentChannelV1ServerState extends PaymentChannelServerState {
throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg);
}
// Now really sign the multisig input.
- signMultisigInput(tx, Transaction.SigHash.ALL, false);
+ signMultisigInput(tx, Transaction.SigHash.ALL, false, userKey);
// Some checks that shouldn't be necessary but it can't hurt to check.
tx.verify(); // Sanity check syntax.
for (TransactionInput input : tx.getInputs())
diff --git a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ServerState.java b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ServerState.java
index f15fcc54..10404ad3 100644
--- a/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ServerState.java
+++ b/core/src/main/java/org/bitcoinj/protocols/channels/PaymentChannelV2ServerState.java
@@ -30,7 +30,9 @@ import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.spongycastle.crypto.params.KeyParameter;
+import javax.annotation.Nullable;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Locale;
@@ -132,8 +134,9 @@ public class PaymentChannelV2ServerState extends PaymentChannelServerState {
}
// Signs the first input of the transaction which must spend the multisig contract.
- private void signP2SHInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) {
- TransactionSignature signature = tx.calculateSignature(0, serverKey, createP2SHRedeemScript(), hashType, anyoneCanPay);
+ private void signP2SHInput(Transaction tx, Transaction.SigHash hashType,
+ boolean anyoneCanPay, @Nullable KeyParameter userKey) {
+ TransactionSignature signature = tx.calculateSignature(0, serverKey, userKey, createP2SHRedeemScript(), hashType, anyoneCanPay);
byte[] mySig = signature.encodeToBitcoin();
Script scriptSig = ScriptBuilder.createCLTVPaymentChannelP2SHInput(bestValueSignature, mySig, createP2SHRedeemScript());
tx.getInput(0).setScriptSig(scriptSig);
@@ -142,7 +145,7 @@ public class PaymentChannelV2ServerState extends PaymentChannelServerState {
final SettableFuture closedFuture = SettableFuture.create();
@Override
- public synchronized ListenableFuture close() throws InsufficientMoneyException {
+ public synchronized ListenableFuture close(@Nullable KeyParameter userKey) throws InsufficientMoneyException {
if (storedServerChannel != null) {
StoredServerChannel temp = storedServerChannel;
storedServerChannel = null;
@@ -172,7 +175,7 @@ public class PaymentChannelV2ServerState extends PaymentChannelServerState {
// know how to sign. Note that this signature does actually have to be valid, so we can't use a dummy
// signature to save time, because otherwise completeTx will try to re-sign it to make it valid and then
// die. We could probably add features to the SendRequest API to make this a bit more efficient.
- signP2SHInput(tx, Transaction.SigHash.NONE, true);
+ signP2SHInput(tx, Transaction.SigHash.NONE, true, userKey);
// Let wallet handle adding additional inputs/fee as necessary.
req.shuffleOutputs = false;
req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG;
@@ -185,7 +188,7 @@ public class PaymentChannelV2ServerState extends PaymentChannelServerState {
throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg);
}
// Now really sign the multisig input.
- signP2SHInput(tx, Transaction.SigHash.ALL, false);
+ signP2SHInput(tx, Transaction.SigHash.ALL, false, userKey);
// Some checks that shouldn't be necessary but it can't hurt to check.
tx.verify(); // Sanity check syntax.
for (TransactionInput input : tx.getInputs())
diff --git a/core/src/test/java/org/bitcoinj/protocols/channels/ChannelTestUtils.java b/core/src/test/java/org/bitcoinj/protocols/channels/ChannelTestUtils.java
index 0f9cd270..36a539db 100644
--- a/core/src/test/java/org/bitcoinj/protocols/channels/ChannelTestUtils.java
+++ b/core/src/test/java/org/bitcoinj/protocols/channels/ChannelTestUtils.java
@@ -24,6 +24,7 @@ import com.google.common.util.concurrent.Futures;
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;
import java.util.concurrent.BlockingQueue;
@@ -59,6 +60,12 @@ public class ChannelTestUtils {
return Futures.immediateFuture(ByteString.copyFromUtf8(by.toPlainString()));
}
+ @Nullable
+ @Override
+ public ListenableFuture getUserKey() {
+ return null;
+ }
+
public Protos.TwoWayChannelMessage getNextMsg() throws InterruptedException {
return (Protos.TwoWayChannelMessage) q.take();
}