3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-13 10:45:51 +00:00

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()`
This commit is contained in:
Will Shackleton 2016-04-08 15:12:12 +01:00 committed by Andreas Schildbach
parent 56f1db8dd8
commit 01cff428d3
6 changed files with 68 additions and 12 deletions

View File

@ -17,6 +17,7 @@
package org.bitcoinj.protocols.channels; package org.bitcoinj.protocols.channels;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.AsyncFunction;
import org.bitcoinj.core.*; import org.bitcoinj.core.*;
import org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason; import org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason;
import org.bitcoinj.utils.Threading; import org.bitcoinj.utils.Threading;
@ -29,6 +30,7 @@ import com.google.protobuf.ByteString;
import net.jcip.annotations.GuardedBy; import net.jcip.annotations.GuardedBy;
import org.bitcoin.paymentchannel.Protos; import org.bitcoin.paymentchannel.Protos;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Map; import java.util.Map;
@ -117,6 +119,13 @@ public class PaymentChannelServer {
*/ */
@Nullable @Nullable
ListenableFuture<ByteString> paymentIncrease(Coin by, Coin to, @Nullable ByteString info); ListenableFuture<ByteString> paymentIncrease(Coin by, Coin to, @Nullable ByteString info);
/**
* <p>Called when a channel is being closed and must be signed, possibly with an encrypted key.</p>
* @return A future for the (nullable) KeyParameter for the ECKey, or <code>null</code> if no key is required.
*/
@Nullable
ListenableFuture<KeyParameter> getUserKey();
} }
private final ServerConnection conn; private final ServerConnection conn;
@ -520,7 +529,19 @@ public class PaymentChannelServer {
// close() on us here below via the stored channel state. // 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. // TODO: Strongly separate the lifecycle of the payment channel from the TCP connection in these classes.
channelSettling = true; channelSettling = true;
Futures.addCallback(state.close(), new FutureCallback<Transaction>() { ListenableFuture<KeyParameter> keyFuture = conn.getUserKey();
ListenableFuture<Transaction> result;
if (keyFuture != null) {
result = Futures.transform(conn.getUserKey(), new AsyncFunction<KeyParameter, Transaction>() {
@Override
public ListenableFuture<Transaction> apply(KeyParameter userKey) throws Exception {
return state.close(userKey);
}
});
} else {
result = state.close();
}
Futures.addCallback(result, new FutureCallback<Transaction>() {
@Override @Override
public void onSuccess(Transaction result) { public void onSuccess(Transaction result) {
// Send the successfully accepted transaction back to the client. // Send the successfully accepted transaction back to the client.

View File

@ -28,6 +28,7 @@ import org.bitcoinj.wallet.Wallet;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.bitcoin.paymentchannel.Protos; import org.bitcoin.paymentchannel.Protos;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -88,6 +89,12 @@ public class PaymentChannelServerListener {
@Override public ListenableFuture<ByteString> paymentIncrease(Coin by, Coin to, @Nullable ByteString info) { @Override public ListenableFuture<ByteString> paymentIncrease(Coin by, Coin to, @Nullable ByteString info) {
return eventHandler.paymentIncrease(by, to, info); return eventHandler.paymentIncrease(by, to, info);
} }
@Nullable
@Override
public ListenableFuture<KeyParameter> getUserKey() {
return null;
}
}); });
protobufHandlerListener = new ProtobufConnection.Listener<Protos.TwoWayChannelMessage>() { protobufHandlerListener = new ProtobufConnection.Listener<Protos.TwoWayChannelMessage>() {

View File

@ -30,6 +30,7 @@ import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script; import org.bitcoinj.script.Script;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -300,7 +301,20 @@ public abstract class PaymentChannelServerState {
* will never complete, a timeout should be used. * 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. * @throws InsufficientMoneyException If the payment tx would have cost more in fees to spend than it is worth.
*/ */
public abstract ListenableFuture<Transaction> close() throws InsufficientMoneyException; public ListenableFuture<Transaction> close() throws InsufficientMoneyException {
return close(null);
}
/**
* <p>Closes this channel and broadcasts the highest value payment transaction on the network.</p>
*
* @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<Transaction> close(@Nullable KeyParameter userKey) throws InsufficientMoneyException;
/** /**
* Gets the highest payment to ourselves (which we will receive on settle(), not including fees) * Gets the highest payment to ourselves (which we will receive on settle(), not including fees)

View File

@ -30,7 +30,9 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
import java.util.Locale; import java.util.Locale;
import static com.google.common.base.Preconditions.*; 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. // Signs the first input of the transaction which must spend the multisig contract.
private void signMultisigInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) { private void signMultisigInput(Transaction tx, Transaction.SigHash hashType,
TransactionSignature signature = tx.calculateSignature(0, serverKey, getContractScript(), hashType, anyoneCanPay); boolean anyoneCanPay, @Nullable KeyParameter userKey) {
TransactionSignature signature = tx.calculateSignature(0, serverKey, userKey, getContractScript(), hashType, anyoneCanPay);
byte[] mySig = signature.encodeToBitcoin(); byte[] mySig = signature.encodeToBitcoin();
Script scriptSig = ScriptBuilder.createMultiSigInputScriptBytes(ImmutableList.of(bestValueSignature, mySig)); Script scriptSig = ScriptBuilder.createMultiSigInputScriptBytes(ImmutableList.of(bestValueSignature, mySig));
tx.getInput(0).setScriptSig(scriptSig); 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. * simply set the state to {@link State#CLOSED} and let the client handle getting its refund transaction confirmed.
* </p> * </p>
* *
* @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 * @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 * broadcast fails for some reason. Note that if the network simply rejects the transaction, this future
* will never complete, a timeout should be used. * 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. * @throws InsufficientMoneyException If the payment tx would have cost more in fees to spend than it is worth.
*/ */
@Override @Override
public synchronized ListenableFuture<Transaction> close() throws InsufficientMoneyException { public synchronized ListenableFuture<Transaction> close(@Nullable KeyParameter userKey) throws InsufficientMoneyException {
if (storedServerChannel != null) { if (storedServerChannel != null) {
StoredServerChannel temp = storedServerChannel; StoredServerChannel temp = storedServerChannel;
storedServerChannel = null; 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 // 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 // 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. // 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. // Let wallet handle adding additional inputs/fee as necessary.
req.shuffleOutputs = false; req.shuffleOutputs = false;
req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG; req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG;
@ -230,7 +234,7 @@ public class PaymentChannelV1ServerState extends PaymentChannelServerState {
throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg); throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg);
} }
// Now really sign the multisig input. // 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. // Some checks that shouldn't be necessary but it can't hurt to check.
tx.verify(); // Sanity check syntax. tx.verify(); // Sanity check syntax.
for (TransactionInput input : tx.getInputs()) for (TransactionInput input : tx.getInputs())

View File

@ -30,7 +30,9 @@ import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.Wallet;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; 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. // Signs the first input of the transaction which must spend the multisig contract.
private void signP2SHInput(Transaction tx, Transaction.SigHash hashType, boolean anyoneCanPay) { private void signP2SHInput(Transaction tx, Transaction.SigHash hashType,
TransactionSignature signature = tx.calculateSignature(0, serverKey, createP2SHRedeemScript(), hashType, anyoneCanPay); boolean anyoneCanPay, @Nullable KeyParameter userKey) {
TransactionSignature signature = tx.calculateSignature(0, serverKey, userKey, createP2SHRedeemScript(), hashType, anyoneCanPay);
byte[] mySig = signature.encodeToBitcoin(); byte[] mySig = signature.encodeToBitcoin();
Script scriptSig = ScriptBuilder.createCLTVPaymentChannelP2SHInput(bestValueSignature, mySig, createP2SHRedeemScript()); Script scriptSig = ScriptBuilder.createCLTVPaymentChannelP2SHInput(bestValueSignature, mySig, createP2SHRedeemScript());
tx.getInput(0).setScriptSig(scriptSig); tx.getInput(0).setScriptSig(scriptSig);
@ -142,7 +145,7 @@ public class PaymentChannelV2ServerState extends PaymentChannelServerState {
final SettableFuture<Transaction> closedFuture = SettableFuture.create(); final SettableFuture<Transaction> closedFuture = SettableFuture.create();
@Override @Override
public synchronized ListenableFuture<Transaction> close() throws InsufficientMoneyException { public synchronized ListenableFuture<Transaction> close(@Nullable KeyParameter userKey) throws InsufficientMoneyException {
if (storedServerChannel != null) { if (storedServerChannel != null) {
StoredServerChannel temp = storedServerChannel; StoredServerChannel temp = storedServerChannel;
storedServerChannel = null; 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 // 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 // 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. // 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. // Let wallet handle adding additional inputs/fee as necessary.
req.shuffleOutputs = false; req.shuffleOutputs = false;
req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG; req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG;
@ -185,7 +188,7 @@ public class PaymentChannelV2ServerState extends PaymentChannelServerState {
throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg); throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg);
} }
// Now really sign the multisig input. // 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. // Some checks that shouldn't be necessary but it can't hurt to check.
tx.verify(); // Sanity check syntax. tx.verify(); // Sanity check syntax.
for (TransactionInput input : tx.getInputs()) for (TransactionInput input : tx.getInputs())

View File

@ -24,6 +24,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.bitcoin.paymentchannel.Protos; import org.bitcoin.paymentchannel.Protos;
import org.spongycastle.crypto.params.KeyParameter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@ -59,6 +60,12 @@ public class ChannelTestUtils {
return Futures.immediateFuture(ByteString.copyFromUtf8(by.toPlainString())); return Futures.immediateFuture(ByteString.copyFromUtf8(by.toPlainString()));
} }
@Nullable
@Override
public ListenableFuture<KeyParameter> getUserKey() {
return null;
}
public Protos.TwoWayChannelMessage getNextMsg() throws InterruptedException { public Protos.TwoWayChannelMessage getNextMsg() throws InterruptedException {
return (Protos.TwoWayChannelMessage) q.take(); return (Protos.TwoWayChannelMessage) q.take();
} }