All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bitcoinj.protocols.channels.PaymentChannelClientState Maven / Gradle / Ivy

There is a newer version: 0.15-cm06
Show newest version
/*
 * 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.base.Throwables;
import com.google.common.collect.Multimap;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientChannelProperties;
import org.bitcoinj.script.Script;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;

import javax.annotation.Nullable;

import static com.google.common.base.Preconditions.*;

/**
 * 

A payment channel is a method of sending money to someone such that the amount of money you send can be adjusted * after the fact, in an efficient manner that does not require broadcasting to the network. This can be used to * implement micropayments or other payment schemes in which immediate settlement is not required, but zero trust * negotiation is. Note that this class only allows the amount of money sent to be incremented, not decremented.

* *

This class has two subclasses, {@link PaymentChannelV1ClientState} and {@link PaymentChannelV2ClientState} for * protocols version 1 and 2.

* *

This class implements the core state machine for the client side of the protocol. The server side is implemented * by {@link PaymentChannelServerState} and {@link PaymentChannelClientConnection} implements a network protocol * suitable for TCP/IP connections which moves this class through each state. We say that the party who is sending funds * is the client or initiating party. The party that is receiving the funds is the server or * receiving party. Although the underlying Bitcoin protocol is capable of more complex relationships than that, * this class implements only the simplest case.

* *

A channel has an expiry parameter. If the server halts after the multi-signature contract which locks * up the given value is broadcast you could get stuck in a state where you've lost all the money put into the * contract. To avoid this, a refund transaction is agreed ahead of time but it may only be used/broadcast after * the expiry time. This is specified in terms of block timestamps and once the timestamp of the chain chain approaches * the given time (within a few hours), the channel must be closed or else the client will broadcast the refund * transaction and take back all the money once the expiry time is reached.

* *

To begin, the client calls {@link PaymentChannelClientState#initiate(KeyParameter, org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientChannelProperties)}, which moves the channel into state * INITIATED and creates the initial multi-sig contract and refund transaction. If the wallet has insufficient funds an * exception will be thrown at this point. Once this is done, call * {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction()} and pass the resultant transaction through to the * server. Once you have retrieved the signature, use {@link PaymentChannelV1ClientState#provideRefundSignature(byte[], KeyParameter)}. * You must then call {@link PaymentChannelClientState#storeChannelInWallet(Sha256Hash)} to store the refund transaction * in the wallet, protecting you against a malicious server attempting to destroy all your coins. At this point, you can * provide the server with the multi-sig contract (via {@link PaymentChannelClientState#getContract()}) safely. *

*/ public abstract class PaymentChannelClientState { private static final Logger log = LoggerFactory.getLogger(PaymentChannelClientState.class); // How much value is currently allocated to us. Starts as being same as totalValue. protected Coin valueToMe; /** * The different logical states the channel can be in. The channel starts out as NEW, and then steps through the * states until it becomes finalized. The server should have already been contacted and asked for a public key * by the time the NEW state is reached. */ public enum State { UNINITIALISED, NEW, INITIATED, WAITING_FOR_SIGNED_REFUND, SAVE_STATE_IN_WALLET, PROVIDE_MULTISIG_CONTRACT_TO_SERVER, READY, EXPIRED, CLOSED } protected final StateMachine stateMachine; final Wallet wallet; // Both sides need a key (private in our case, public for the server) in order to manage the multisig contract // and transactions that spend it. final ECKey myKey, serverKey; // The id of this channel in the StoredPaymentChannelClientStates, or null if it is not stored protected StoredClientChannel storedChannel; PaymentChannelClientState(StoredClientChannel storedClientChannel, Wallet wallet) throws VerificationException { this.stateMachine = new StateMachine<>(State.UNINITIALISED, getStateTransitions()); this.wallet = checkNotNull(wallet); this.myKey = checkNotNull(storedClientChannel.myKey); this.serverKey = checkNotNull(storedClientChannel.serverKey); this.storedChannel = storedClientChannel; this.valueToMe = checkNotNull(storedClientChannel.valueToMe); } /** * Returns true if the tx is a valid settlement transaction. */ public synchronized boolean isSettlementTransaction(Transaction tx) { try { tx.verify(); tx.getInput(0).verify(getContractInternal().getOutput(0)); return true; } catch (VerificationException e) { return false; } } /** * Creates a state object for a payment channel client. It is expected that you be ready to * {@link PaymentChannelClientState#initiate(KeyParameter, org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientChannelProperties)} after construction (to avoid creating objects for channels which are * not going to finish opening) and thus some parameters provided here are only used in * {@link PaymentChannelClientState#initiate(KeyParameter, org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientChannelProperties)} to create the Multisig contract and refund transaction. * * @param wallet a wallet that contains at least the specified amount of value. * @param myKey a freshly generated private key for this channel. * @param serverKey a public key retrieved from the server used for the initial multisig contract * @param value how many satoshis to put into this contract. If the channel reaches this limit, it must be closed. * @param expiryTimeInSeconds At what point (UNIX timestamp +/- a few hours) the channel will expire * * @throws VerificationException If either myKey's pubkey or serverKey's pubkey are non-canonical (ie invalid) */ public PaymentChannelClientState(Wallet wallet, ECKey myKey, ECKey serverKey, Coin value, long expiryTimeInSeconds) throws VerificationException { this.stateMachine = new StateMachine<>(State.UNINITIALISED, getStateTransitions()); this.wallet = checkNotNull(wallet); this.serverKey = checkNotNull(serverKey); this.myKey = checkNotNull(myKey); this.valueToMe = checkNotNull(value); } protected synchronized void initWalletListeners() { // Register a listener that watches out for the server closing the channel. if (storedChannel != null && storedChannel.close != null) { watchCloseConfirmations(); } wallet.addCoinsReceivedEventListener(Threading.SAME_THREAD, new WalletCoinsReceivedEventListener() { @Override public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { synchronized (PaymentChannelClientState.this) { if (getContractInternal() == null) return; if (isSettlementTransaction(tx)) { log.info("Close: transaction {} closed contract {}", tx.getHash(), getContractInternal().getHash()); // Record the fact that it was closed along with the transaction that closed it. stateMachine.transition(State.CLOSED); if (storedChannel == null) return; storedChannel.close = tx; updateChannelInWallet(); watchCloseConfirmations(); } } } }); } protected void watchCloseConfirmations() { // When we see the close transaction get enough confirmations, we can just delete the record // of this channel along with the refund tx from the wallet, because we're not going to need // any of that any more. final TransactionConfidence confidence = storedChannel.close.getConfidence(); int numConfirms = Context.get().getEventHorizon(); ListenableFuture future = confidence.getDepthFuture(numConfirms, Threading.SAME_THREAD); Futures.addCallback(future, new FutureCallback() { @Override public void onSuccess(TransactionConfidence result) { deleteChannelFromWallet(); } @Override public void onFailure(Throwable t) { Throwables.propagate(t); } }); } private synchronized void deleteChannelFromWallet() { log.info("Close tx has confirmed, deleting channel from wallet: {}", storedChannel); StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); channels.removeChannel(storedChannel); storedChannel = null; } public synchronized State getState() { return stateMachine.getState(); } protected abstract Multimap getStateTransitions(); public abstract int getMajorVersion(); /** * Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate * time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and * {@link PaymentChannelV1ClientState#getContract()}. The way the contract is crafted can be adjusted by * By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low. * * @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network * @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate */ public void initiate() throws ValueOutOfRangeException, InsufficientMoneyException { initiate(null, PaymentChannelClient.defaultChannelProperties); } /** * Creates the initial multisig contract and incomplete refund transaction which can be requested at the appropriate * time using {@link PaymentChannelV1ClientState#getIncompleteRefundTransaction} and * {@link PaymentChannelClientState#getContract()}. * By default unconfirmed coins are allowed to be used, as for micropayments the risk should be relatively low. * @param userKey Key derived from a user password, needed for any signing when the wallet is encrypted. * The wallet KeyCrypter is assumed. * @param clientChannelProperties Modify the channel's configuration. * * @throws ValueOutOfRangeException if the value being used is too small to be accepted by the network * @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate */ public abstract void initiate(@Nullable KeyParameter userKey, ClientChannelProperties clientChannelProperties) throws ValueOutOfRangeException, InsufficientMoneyException; /** * Gets the contract which was used to initialize this channel */ public abstract Transaction getContract(); private synchronized Transaction makeUnsignedChannelContract(Coin valueToMe) throws ValueOutOfRangeException { Transaction tx = new Transaction(wallet.getParams()); tx.addInput(getContractInternal().getOutput(0)); // Our output always comes first. // TODO: We should drop myKey in favor of output key + multisig key separation // (as its always obvious who the client is based on T2 output order) tx.addOutput(valueToMe, myKey.toAddress(wallet.getParams())); return tx; } /** * Checks if the channel is expired, setting state to {@link State#EXPIRED}, removing this channel from wallet * storage and throwing an {@link IllegalStateException} if it is. */ public synchronized void checkNotExpired() { if (Utils.currentTimeSeconds() > getExpiryTime()) { stateMachine.transition(State.EXPIRED); disconnectFromChannel(); throw new IllegalStateException("Channel expired"); } } /** Container for a signature and an amount that was sent. */ public static class IncrementedPayment { public TransactionSignature signature; public Coin amount; } /** *

Updates the outputs on the payment contract transaction and re-signs it. The state must be READY in order to * call this method. The signature that is returned should be sent to the server so it has the ability to broadcast * the best seen payment when the channel closes or times out.

* *

The returned signature is over the payment transaction, which we never have a valid copy of and thus there * is no accessor for it on this object.

* *

To spend the whole channel increment by {@link PaymentChannelV1ClientState#getTotalValue()} - * {@link PaymentChannelV1ClientState#getValueRefunded()}

* * @param size How many satoshis to increment the payment by (note: not the new total). * @throws ValueOutOfRangeException If size is negative or the channel does not have sufficient money in it to * complete this payment. */ public synchronized IncrementedPayment incrementPaymentBy(Coin size, @Nullable KeyParameter userKey) throws ValueOutOfRangeException { stateMachine.checkState(State.READY); checkNotExpired(); checkNotNull(size); // Validity of size will be checked by makeUnsignedChannelContract. if (size.signum() < 0) throw new ValueOutOfRangeException("Tried to decrement payment"); Coin newValueToMe = getValueToMe().subtract(size); if (newValueToMe.compareTo(Transaction.MIN_NONDUST_OUTPUT) < 0 && newValueToMe.signum() > 0) { log.info("New value being sent back as change was smaller than minimum nondust output, sending all"); size = getValueToMe(); newValueToMe = Coin.ZERO; } if (newValueToMe.signum() < 0) throw new ValueOutOfRangeException("Channel has too little money to pay " + size + " satoshis"); Transaction tx = makeUnsignedChannelContract(newValueToMe); log.info("Signing new payment tx {}", tx); Transaction.SigHash mode; // If we spent all the money we put into this channel, we (by definition) don't care what the outputs are, so // we sign with SIGHASH_NONE to let the server do what it wants. if (newValueToMe.equals(Coin.ZERO)) mode = Transaction.SigHash.NONE; else mode = Transaction.SigHash.SINGLE; TransactionSignature sig = tx.calculateSignature(0, myKey.maybeDecrypt(userKey), getSignedScript(), mode, true); valueToMe = newValueToMe; updateChannelInWallet(); IncrementedPayment payment = new IncrementedPayment(); payment.signature = sig; payment.amount = size; return payment; } protected synchronized void updateChannelInWallet() { if (storedChannel == null) return; storedChannel.valueToMe = getValueToMe(); StoredPaymentChannelClientStates channels = (StoredPaymentChannelClientStates) wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID); channels.updatedChannel(storedChannel); } /** * Sets this channel's state in {@link StoredPaymentChannelClientStates} to unopened so this channel can be reopened * later. * * @see PaymentChannelV1ClientState#storeChannelInWallet(Sha256Hash) */ public synchronized void disconnectFromChannel() { if (storedChannel == null) return; synchronized (storedChannel) { storedChannel.active = false; } } /** * Skips saving state in the wallet for testing */ @VisibleForTesting synchronized void fakeSave() { try { wallet.commitTx(getContractInternal()); } catch (VerificationException e) { throw new RuntimeException(e); // We created it } stateMachine.transition(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER); } @VisibleForTesting abstract void doStoreChannelInWallet(Sha256Hash id); /** *

Stores this channel's state in the wallet as a part of a {@link StoredPaymentChannelClientStates} wallet * extension and keeps it up-to-date each time payment is incremented. This allows the * {@link StoredPaymentChannelClientStates} object to keep track of timeouts and broadcast the refund transaction * when the channel expires.

* *

A channel may only be stored after it has fully opened (ie state == State.READY). The wallet provided in the * constructor must already have a {@link StoredPaymentChannelClientStates} object in its extensions set.

* * @param id A hash providing this channel with an id which uniquely identifies this server. It does not have to be * unique. */ public synchronized void storeChannelInWallet(Sha256Hash id) { stateMachine.checkState(State.SAVE_STATE_IN_WALLET); checkState(id != null); if (storedChannel != null) { checkState(storedChannel.id.equals(id)); return; } doStoreChannelInWallet(id); try { wallet.commitTx(getContractInternal()); } catch (VerificationException e) { throw new RuntimeException(e); // We created it } stateMachine.transition(State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER); } /** * Returns the fees that will be paid if the refund transaction has to be claimed because the server failed to settle * the channel properly. May only be called after {@link PaymentChannelClientState#initiate(KeyParameter, org.bitcoinj.protocols.channels.IPaymentChannelClient.ClientChannelProperties)} */ public abstract Coin getRefundTxFees(); @VisibleForTesting abstract Transaction getRefundTransaction(); /** * Gets the total value of this channel (ie the maximum payment possible) */ public abstract Coin getTotalValue(); /** * Gets the current amount refunded to us from the multisig contract (ie totalValue-valueSentToServer) */ public synchronized Coin getValueRefunded() { stateMachine.checkState(State.READY); return valueToMe; } /** * Returns the amount of money sent on this channel so far. */ public synchronized Coin getValueSpent() { return getTotalValue().subtract(getValueRefunded()); } protected abstract Coin getValueToMe(); protected abstract long getExpiryTime(); /** * Gets the contract without changing the state machine * @return the contract. */ protected abstract Transaction getContractInternal(); protected abstract Script getContractScript(); /** * Gets the script that is signed. In the case of a P2SH contract this is the * script inside the P2SH script. * @return the signed script. */ protected abstract Script getSignedScript(); }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy