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