org.bitcoinj.protocols.channels.PaymentChannelV2ClientState Maven / Gradle / Ivy
/*
* 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.Multimap;
import com.google.common.collect.MultimapBuilder;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientChannelProperties;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.wallet.AllowUnconfirmedCoinSelector;
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 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 getStateTransitions() {
Multimap 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, ClientChannelProperties clientChannelProperties) 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.isDust())
throw new ValueOutOfRangeException("totalValue too small to use");
SendRequest req = SendRequest.forTx(template);
req.coinSelector = AllowUnconfirmedCoinSelector.get();
req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable.
req = clientChannelProperties.modifyContractSendRequest(req);
if (userKey != null) 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 because the
// CheckLockTimeVerify opcode requires a lock time to be specified and the input to have a non-final sequence
// number (so that the lock time is not disabled).
refundTx = new Transaction(params);
// by using this sequence value, we avoid extra full replace-by-fee and relative lock time processing.
refundTx.addInput(contract.getOutput(0)).setSequenceNumber(TransactionInput.NO_SEQUENCE - 1L);
refundTx.setLockTime(expiryTime);
if (Context.get().isEnsureMinRequiredFee()) {
// 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;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy