Commit 8af0fa98 authored by Will Shackleton's avatar Will Shackleton Committed by Andreas Schildbach

Implemented version 2 of payment channels API

I implemented version 2 of the payment channels API using
OP_CHECKLOCKTIMEVERIFY-style payment channels.
parent 4b2afc96
......@@ -3818,11 +3818,12 @@ public class Wallet extends BaseTaggableObject
return toCLTVPaymentChannel(params, BigInteger.valueOf(time), from, to, value);
}
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, long lockTime, ECKey from, ECKey to, Coin value) {
return toCLTVPaymentChannel(params, BigInteger.valueOf(lockTime), from, to, value);
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, int releaseBlock, ECKey from, ECKey to, Coin value) {
checkArgument(0 <= releaseBlock && releaseBlock < Transaction.LOCKTIME_THRESHOLD, "Block number was too large");
return toCLTVPaymentChannel(params, BigInteger.valueOf(releaseBlock), from, to, value);
}
private static SendRequest toCLTVPaymentChannel(NetworkParameters params, BigInteger time, ECKey from, ECKey to, Coin value) {
public static SendRequest toCLTVPaymentChannel(NetworkParameters params, BigInteger time, ECKey from, ECKey to, Coin value) {
SendRequest req = new SendRequest();
Script output = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to);
req.tx = new Transaction(params);
......@@ -4223,6 +4224,7 @@ public class Wallet extends BaseTaggableObject
log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i);
continue;
} catch (ScriptException e) {
log.debug("Input contained an incorrect signature", e);
// Expected.
}
......
......@@ -16,6 +16,7 @@
package org.bitcoinj.protocols.channels;
import com.google.common.collect.ImmutableMap;
import org.bitcoinj.core.*;
import org.bitcoinj.protocols.channels.PaymentChannelCloseException.CloseReason;
import org.bitcoinj.utils.Threading;
......@@ -28,6 +29,7 @@ import org.bitcoin.paymentchannel.Protos;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.checkNotNull;
......@@ -47,12 +49,17 @@ public class PaymentChannelServer {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(PaymentChannelServer.class);
protected final ReentrantLock lock = Threading.lock("channelserver");
public final int SERVER_MAJOR_VERSION = 1;
public final int SERVER_MINOR_VERSION = 0;
/**
* A map of supported versions; keys are major versions, and the corresponding
* value is the minor version at that major level.
*/
public static final Map<Integer, Integer> SERVER_VERSIONS = ImmutableMap.of(1, 0, 2, 0);
// The step in the initialization process we are in, some of this is duplicated in the PaymentChannelServerState
private enum InitStep {
WAITING_ON_CLIENT_VERSION,
// This step is only used in V1 of the protocol.
WAITING_ON_UNSIGNED_REFUND,
WAITING_ON_CONTRACT,
WAITING_ON_MULTISIG_ACCEPTANCE,
......@@ -111,6 +118,9 @@ public class PaymentChannelServer {
}
private final ServerConnection conn;
// Used to track the negotiated version number
@GuardedBy("lock") private int majorVersion;
// Used to keep track of whether or not the "socket" ie connection is open and we can generate messages
@GuardedBy("lock") private boolean connectionOpen = false;
// Indicates that no further messages should be sent and we intend to settle the connection
......@@ -211,15 +221,15 @@ public class PaymentChannelServer {
private void receiveVersionMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
checkState(step == InitStep.WAITING_ON_CLIENT_VERSION && msg.hasClientVersion());
final Protos.ClientVersion clientVersion = msg.getClientVersion();
final int major = clientVersion.getMajor();
if (major != SERVER_MAJOR_VERSION) {
error("This server needs protocol version " + SERVER_MAJOR_VERSION + " , client offered " + major,
majorVersion = clientVersion.getMajor();
if (!SERVER_VERSIONS.containsKey(majorVersion)) {
error("This server needs one of protocol versions " + SERVER_VERSIONS.keySet() + " , client offered " + majorVersion,
Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION, CloseReason.NO_ACCEPTABLE_VERSION);
return;
}
Protos.ServerVersion.Builder versionNegotiationBuilder = Protos.ServerVersion.newBuilder()
.setMajor(SERVER_MAJOR_VERSION).setMinor(SERVER_MINOR_VERSION);
.setMajor(majorVersion).setMinor(SERVER_VERSIONS.get(majorVersion));
conn.sendToClient(Protos.TwoWayChannelMessage.newBuilder()
.setType(Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION)
.setServerVersion(versionNegotiationBuilder)
......@@ -262,7 +272,17 @@ public class PaymentChannelServer {
wallet.freshReceiveKey();
expireTime = Utils.currentTimeSeconds() + truncateTimeWindow(clientVersion.getTimeWindowSecs());
step = InitStep.WAITING_ON_UNSIGNED_REFUND;
switch (majorVersion) {
case 1:
step = InitStep.WAITING_ON_UNSIGNED_REFUND;
break;
case 2:
step = InitStep.WAITING_ON_CONTRACT;
break;
default:
error("Protocol version " + majorVersion + " not supported", Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION, CloseReason.NO_ACCEPTABLE_VERSION);
break;
}
Protos.Initiate.Builder initiateBuilder = Protos.Initiate.newBuilder()
.setMultisigKey(ByteString.copyFrom(myKey.getPubKey()))
......@@ -290,13 +310,16 @@ public class PaymentChannelServer {
@GuardedBy("lock")
private void receiveRefundMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
checkState(majorVersion == 1);
checkState(step == InitStep.WAITING_ON_UNSIGNED_REFUND && msg.hasProvideRefund());
log.info("Got refund transaction, returning signature");
Protos.ProvideRefund providedRefund = msg.getProvideRefund();
state = new PaymentChannelServerState(broadcaster, wallet, myKey, expireTime);
byte[] signature = state.provideRefundTransaction(wallet.getParams().getDefaultSerializer().makeTransaction(providedRefund.getTx().toByteArray()),
providedRefund.getMultisigKey().toByteArray());
state = new PaymentChannelV1ServerState(broadcaster, wallet, myKey, expireTime);
// We can cast to V1 state since this state is only used in the V1 protocol
byte[] signature = ((PaymentChannelV1ServerState) state)
.provideRefundTransaction(wallet.getParams().getDefaultSerializer().makeTransaction(providedRefund.getTx().toByteArray()),
providedRefund.getMultisigKey().toByteArray());
step = InitStep.WAITING_ON_CONTRACT;
......@@ -344,18 +367,25 @@ public class PaymentChannelServer {
@GuardedBy("lock")
private void receiveContractMessage(Protos.TwoWayChannelMessage msg) throws VerificationException {
checkState(majorVersion == 1 || majorVersion == 2);
checkState(step == InitStep.WAITING_ON_CONTRACT && msg.hasProvideContract());
log.info("Got contract, broadcasting and responding with CHANNEL_OPEN");
final Protos.ProvideContract providedContract = msg.getProvideContract();
if (majorVersion == 2) {
state = new PaymentChannelV2ServerState(broadcaster, wallet, myKey, expireTime);
checkState(providedContract.hasClientKey(), "ProvideContract didn't have a client key in protocol v2");
((PaymentChannelV2ServerState)state).provideClientKey(providedContract.getClientKey().toByteArray());
}
//TODO notify connection handler that timeout should be significantly extended as we wait for network propagation?
final Transaction multisigContract = wallet.getParams().getDefaultSerializer().makeTransaction(providedContract.getTx().toByteArray());
final Transaction contract = wallet.getParams().getDefaultSerializer().makeTransaction(providedContract.getTx().toByteArray());
step = InitStep.WAITING_ON_MULTISIG_ACCEPTANCE;
state.provideMultiSigContract(multisigContract)
state.provideContract(contract)
.addListener(new Runnable() {
@Override
public void run() {
multisigContractPropogated(providedContract, multisigContract.getHash());
multisigContractPropogated(providedContract, contract.getHash());
}
}, Threading.SAME_THREAD);
}
......@@ -413,9 +443,6 @@ public class PaymentChannelServer {
checkState(connectionOpen);
if (channelSettling)
return;
// If we generate an error, we set errorBuilder and closeReason and break, otherwise we return
Protos.Error.Builder errorBuilder;
CloseReason closeReason;
try {
switch (msg.getType()) {
case CLIENT_VERSION:
......@@ -532,18 +559,18 @@ public class PaymentChannelServer {
connectionOpen = false;
try {
if (state != null && state.getMultisigContract() != null) {
if (state != null && state.getContract() != null) {
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)
wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID);
if (channels != null) {
StoredServerChannel storedServerChannel = channels.getChannel(state.getMultisigContract().getHash());
StoredServerChannel storedServerChannel = channels.getChannel(state.getContract().getHash());
if (storedServerChannel != null) {
storedServerChannel.clearConnectedHandler();
}
}
}
} catch (IllegalStateException e) {
// Expected when we call getMultisigContract() sometimes
// Expected when we call getContract() sometimes
}
} finally {
lock.unlock();
......
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.wallet.AllowUnconfirmedCoinSelector;
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.List;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* Version 2 of the payment channel state machine - uses CLTV opcode transactions
* instead of multisig transactions.
*/
public class PaymentChannelV2ClientState extends PaymentChannelClientState {
private static final Logger log = LoggerFactory.getLogger(PaymentChannelV1ClientState.class);
// How much value (in satoshis) is locked up into the channel.
private final Coin totalValue;
// When the channel will automatically settle in favor of the client, if the server halts before protocol termination
// specified in terms of block timestamps (so it can off real time by a few hours).
private final long expiryTime;
// The refund is a time locked transaction that spends all the money of the channel back to the client.
// Unlike in V1 this refund isn't signed by the server - we only have to sign it ourselves.
@VisibleForTesting Transaction refundTx;
private Coin refundFees;
// The multi-sig contract locks the value of the channel up such that the agreement of both parties is required
// to spend it.
private Transaction contract;
PaymentChannelV2ClientState(StoredClientChannel storedClientChannel, Wallet wallet) throws VerificationException {
super(storedClientChannel, wallet);
// The PaymentChannelClientConnection handles storedClientChannel.active and ensures we aren't resuming channels
this.contract = checkNotNull(storedClientChannel.contract);
this.expiryTime = storedClientChannel.expiryTime;
this.totalValue = contract.getOutput(0).getValue();
this.valueToMe = checkNotNull(storedClientChannel.valueToMe);
this.refundTx = checkNotNull(storedClientChannel.refund);
this.refundFees = checkNotNull(storedClientChannel.refundFees);
stateMachine.transition(State.READY);
initWalletListeners();
}
public PaymentChannelV2ClientState(Wallet wallet, ECKey myKey, ECKey serverMultisigKey, Coin value, long expiryTimeInSeconds) throws VerificationException {
super(wallet, myKey, serverMultisigKey, value, expiryTimeInSeconds);
checkArgument(value.signum() > 0);
initWalletListeners();
this.valueToMe = this.totalValue = checkNotNull(value);
this.expiryTime = expiryTimeInSeconds;
stateMachine.transition(State.NEW);
}
@Override
protected Multimap<State, State> getStateTransitions() {
Multimap<State, State> result = MultimapBuilder.enumKeys(State.class).arrayListValues().build();
result.put(State.UNINITIALISED, State.NEW);
result.put(State.UNINITIALISED, State.READY);
result.put(State.NEW, State.SAVE_STATE_IN_WALLET);
result.put(State.SAVE_STATE_IN_WALLET, State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER);
result.put(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER, State.READY);
result.put(State.READY, State.EXPIRED);
result.put(State.READY, State.CLOSED);
return result;
}
@Override
public int getMajorVersion() {
return 2;
}
@Override
public synchronized void initiate(@Nullable KeyParameter userKey) throws ValueOutOfRangeException, InsufficientMoneyException {
final NetworkParameters params = wallet.getParams();
Transaction template = new Transaction(params);
// There is also probably a change output, but we don't bother shuffling them as it's obvious from the
// format which one is the change. If we start obfuscating the change output better in future this may
// be worth revisiting.
Script redeemScript =
ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(expiryTime), myKey, serverKey);
TransactionOutput transactionOutput = template.addOutput(totalValue,
ScriptBuilder.createP2SHOutputScript(redeemScript));
if (transactionOutput.getMinNonDustValue().compareTo(totalValue) > 0)
throw new ValueOutOfRangeException("totalValue too small to use");
Wallet.SendRequest req = Wallet.SendRequest.forTx(template);
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();
contract = req.tx;
// Build a refund transaction that protects us in the case of a bad server that's just trying to cause havoc
// by locking up peoples money (perhaps as a precursor to a ransom attempt). We time lock it so the server
// has an assurance that we cannot take back our money by claiming a refund before the channel closes - this
// relies on the fact that since Bitcoin 0.8 time locked transactions are non-final. This will need to change
// in future as it breaks the intended design of timelocking/tx replacement, but for now it simplifies this
// specific protocol somewhat.
refundTx = new Transaction(params);
refundTx.addInput(contract.getOutput(0)).setSequenceNumber(0); // Allow replacement when it's eventually reactivated.
refundTx.setLockTime(expiryTime);
if (totalValue.compareTo(Coin.CENT) < 0) {
// Must pay min fee.
final Coin valueAfterFee = totalValue.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
if (Transaction.MIN_NONDUST_OUTPUT.compareTo(valueAfterFee) > 0)
throw new ValueOutOfRangeException("totalValue too small to use");
refundTx.addOutput(valueAfterFee, myKey.toAddress(params));
refundFees = multisigFee.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
} else {
refundTx.addOutput(totalValue, myKey.toAddress(params));
refundFees = multisigFee;
}
TransactionSignature refundSignature =
refundTx.calculateSignature(0, myKey.maybeDecrypt(userKey),
getSignedScript(), Transaction.SigHash.ALL, false);
refundTx.getInput(0).setScriptSig(ScriptBuilder.createCLTVPaymentChannelP2SHRefund(refundSignature, redeemScript));
refundTx.getConfidence().setSource(TransactionConfidence.Source.SELF);
log.info("initiated channel with contract {}", contract.getHashAsString());
stateMachine.transition(State.SAVE_STATE_IN_WALLET);
// Client should now call getIncompleteRefundTransaction() and send it to the server.
}
@Override
protected synchronized Coin getValueToMe() {
return valueToMe;
}
protected long getExpiryTime() {
return expiryTime;
}
@Override
public synchronized Transaction getContract() {
checkState(contract != null);
if (stateMachine.getState() == State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER) {
stateMachine.transition(State.READY);
}
return contract;
}
@Override
protected synchronized Transaction getContractInternal() {
return contract;
}
protected synchronized Script getContractScript() {
return contract.getOutput(0).getScriptPubKey();
}
@Override
protected Script getSignedScript() {
return ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(expiryTime), myKey, serverKey);
}
@Override
public synchronized Coin getRefundTxFees() {
checkState(getState().compareTo(State.NEW) > 0);
return refundFees;
}
@VisibleForTesting Transaction getRefundTransaction() {
return refundTx;
}
@Override
@VisibleForTesting synchronized void doStoreChannelInWallet(Sha256Hash id) {
StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates)
wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
checkNotNull(channels, "You have not added the StoredPaymentChannelClientStates extension to the wallet.");
checkState(channels.getChannel(id, contract.getHash()) == null);
storedChannel = new StoredClientChannel(getMajorVersion(), id, contract, refundTx, myKey, serverKey, valueToMe, refundFees, expiryTime, true);
channels.putChannel(storedChannel);
}
@Override
public Coin getTotalValue() {
return totalValue;
}
}
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.protocols.channels;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Locale;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
/**
* Version 2 of the payment channel state machine - uses CLTV opcode transactions
* instead of multisig transactions.
*/
public class PaymentChannelV2ServerState extends PaymentChannelServerState {
private static final Logger log = LoggerFactory.getLogger(PaymentChannelV1ServerState.class);
// The total value locked into the CLTV output and the value to us in the last signature the client provided
private Coin feePaidForPayment;
// The client key for the multi-sig contract
// We currently also use the serverKey for payouts, but this is not required
protected ECKey clientKey;
PaymentChannelV2ServerState(StoredServerChannel storedServerChannel, Wallet wallet, TransactionBroadcaster broadcaster) throws VerificationException {
super(storedServerChannel, wallet, broadcaster);
synchronized (storedServerChannel) {
this.clientKey = storedServerChannel.clientKey;
stateMachine.transition(State.READY);
}
}
public PaymentChannelV2ServerState(TransactionBroadcaster broadcaster, Wallet wallet, ECKey serverKey, long minExpireTime) {
super(broadcaster, wallet, serverKey, minExpireTime);
stateMachine.transition(State.WAITING_FOR_MULTISIG_CONTRACT);
}
@Override
public Multimap<State, State> getStateTransitions() {
Multimap<State, State> result = MultimapBuilder.enumKeys(State.class).arrayListValues().build();
result.put(State.UNINITIALISED, State.READY);
result.put(State.UNINITIALISED, State.WAITING_FOR_MULTISIG_CONTRACT);
result.put(State.WAITING_FOR_MULTISIG_CONTRACT, State.WAITING_FOR_MULTISIG_ACCEPTANCE);
result.put(State.WAITING_FOR_MULTISIG_ACCEPTANCE, State.READY);
result.put(State.READY, State.CLOSING);
result.put(State.CLOSING, State.CLOSED);
for (State state : State.values()) {
result.put(state, State.ERROR);
}
return result;
}
@Override
public int getMajorVersion() {
return 2;
}
@Override
public TransactionOutput getClientOutput() {
return null;
}
public void provideClientKey(byte[] clientKey) {
this.clientKey = ECKey.fromPublicOnly(clientKey);
}
@Override
public synchronized Coin getFeePaid() {
stateMachine.checkState(State.CLOSED, State.CLOSING);
return feePaidForPayment;
}
@Override
protected Script getSignedScript() {
return createP2SHRedeemScript();
}
@Override
protected void verifyContract(final Transaction contract) {
super.verifyContract(contract);
// Check contract matches P2SH hash
byte[] expected = getContractScript().getPubKeyHash();
byte[] actual = Utils.sha256hash160(createP2SHRedeemScript().getProgram());
if (!Arrays.equals(actual, expected)) {
throw new VerificationException(
"P2SH hash didn't match required contract - contract should be a CLTV micropayment channel to client and server in that order.");
}
}
/**
* Creates a P2SH script outputting to the client and server pubkeys
* @return
*/
@Override
protected Script createOutputScript() {
return ScriptBuilder.createP2SHOutputScript(createP2SHRedeemScript());
}
private Script createP2SHRedeemScript() {
return ScriptBuilder.createCLTVPaymentChannelOutput(BigInteger.valueOf(getExpiryTime()), clientKey, serverKey);
}
protected ECKey getClientKey() {
return clientKey;
}
// 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);
byte[] mySig = signature.encodeToBitcoin();
Script scriptSig = ScriptBuilder.createCLTVPaymentChannelP2SHInput(bestValueSignature, mySig, createP2SHRedeemScript());
tx.getInput(0).setScriptSig(scriptSig);
}
final SettableFuture<Transaction> closedFuture = SettableFuture.create();
@Override
public synchronized ListenableFuture<Transaction> close() throws InsufficientMoneyException {
if (storedServerChannel != null) {
StoredServerChannel temp = storedServerChannel;
storedServerChannel = null;
StoredPaymentChannelServerStates channels = (StoredPaymentChannelServerStates)
wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID);
channels.closeChannel(temp); // May call this method again for us (if it wasn't the original caller)
if (getState().compareTo(State.CLOSING) >= 0)
return closedFuture;
}
if (getState().ordinal() < State.READY.ordinal()) {
log.error("Attempt to settle channel in state " + getState());
stateMachine.transition(State.CLOSED);
closedFuture.set(null);
return closedFuture;
}
if (getState() != State.READY) {
// TODO: What is this codepath for?
log.warn("Failed attempt to settle a channel in state " + getState());
return closedFuture;
}
Transaction tx = null;
try {
Wallet.SendRequest req = makeUnsignedChannelContract(bestValueToMe);
tx = req.tx;
// Provide a throwaway signature so that completeTx won't complain out about unsigned inputs it doesn't
// 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);
// Let wallet handle adding additional inputs/fee as necessary.
req.shuffleOutputs = false;
req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG;
wallet.completeTx(req); // TODO: Fix things so shuffling is usable.
feePaidForPayment = req.tx.getFee();
log.info("Calculated fee is {}", feePaidForPayment);
if (feePaidForPayment.compareTo(bestValueToMe) > 0) {
final String msg = String.format(Locale.US, "Had to pay more in fees (%s) than the channel was worth (%s)",
feePaidForPayment, bestValueToMe);
throw new InsufficientMoneyException(feePaidForPayment.subtract(bestValueToMe), msg);
}
// Now really sign the multisig input.
signP2SHInput(tx, Transaction.SigHash.ALL, false);
// Some checks that shouldn't be necessary but it can't hurt to check.
tx.verify(); // Sanity check syntax.
for (TransactionInput input : tx.getInputs())
input.verify(); // Run scripts and ensure it is valid.
} catch (InsufficientMoneyException e) {
throw e; // Don't fall through.
} catch (Exception e) {
log.error("Could not verify self-built tx\nMULTISIG {}\nCLOSE {}", contract, tx != null ? tx : "");
throw new RuntimeException(e); // Should never happen.
}
stateMachine.transition(State.CLOSING);
log.info("Closing channel, broadcasting tx {}", tx);
// The act of broadcasting the transaction will add it to the wallet.
ListenableFuture<Transaction> future = broadcaster.broadcastTransaction(tx).future();
Futures.addCallback(future, new FutureCallback<Transaction>() {
@Override public void onSuccess(Transaction transaction) {
log.info("TX {} propagated, channel successfully closed.", transaction.getHash());
stateMachine.transition(State.CLOSED);
closedFuture.set(transaction);
}
@Override public void onFailure(Throwable throwable) {
log.error("Failed to settle channel, could not broadcast: {}", throwable);
stateMachine.transition(State.ERROR);
closedFuture.setException(throwable);
}
});
return closedFuture;
}
}
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either e