Ejemplo n.º 1
0
  /**
   * Add a wallet to the BlockChain. Note that the wallet will be unaffected by any blocks received
   * while it was not part of this BlockChain. This method is useful if the wallet has just been
   * created, and its keys have never been in use, or if the wallet has been loaded along with the
   * BlockChain. Note that adding multiple wallets is not well tested!
   */
  public void addWallet(Wallet wallet) {
    addListener(wallet, Threading.SAME_THREAD);
    int walletHeight = wallet.getLastBlockSeenHeight();
    int chainHeight = getBestChainHeight();
    if (walletHeight != chainHeight) {
      log.warn("Wallet/chain height mismatch: {} vs {}", walletHeight, chainHeight);
      log.warn(
          "Hashes: {} vs {}", wallet.getLastBlockSeenHash(), getChainHead().getHeader().getHash());

      // This special case happens when the VM crashes because of a transaction received. It causes
      // the updated
      // block store to persist, but not the wallet. In order to fix the issue, we roll back the
      // block store to
      // the wallet height to make it look like as if the block has never been received.
      if (walletHeight < chainHeight && walletHeight > 0) {
        try {
          rollbackBlockStore(walletHeight);
          log.info("Rolled back block store to height {}.", walletHeight);
        } catch (BlockStoreException x) {
          log.warn(
              "Rollback of block store failed, continuing with mismatched heights. This can happen due to a replay.");
        }
      }
    }
  }
 private synchronized void updateChannelInWallet() {
   if (storedChannel == null) return;
   storedChannel.valueToMe = valueToMe;
   StoredPaymentChannelClientStates channels =
       (StoredPaymentChannelClientStates)
           wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
   wallet.addOrUpdateExtension(channels);
 }
 /**
  * Creates the initial multisig contract and incomplete refund transaction which can be requested
  * at the appropriate time using {@link PaymentChannelClientState#getIncompleteRefundTransaction}
  * and {@link PaymentChannelClientState#getMultisigContract()}. The way the contract is crafted
  * can be adjusted by overriding {@link
  * PaymentChannelClientState#editContractSendRequest(com.google.bitcoin.core.Wallet.SendRequest)}.
  * 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 synchronized void initiate() throws ValueOutOfRangeException, InsufficientMoneyException {
   final NetworkParameters params = wallet.getParams();
   Transaction template = new Transaction(params);
   // We always place the client key before the server key because, if either side wants some
   // privacy, they can
   // use a fresh key for the the multisig contract and nowhere else
   List<ECKey> keys = Lists.newArrayList(myKey, serverMultisigKey);
   // 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.
   TransactionOutput multisigOutput =
       template.addOutput(totalValue, ScriptBuilder.createMultiSigOutputScript(2, keys));
   if (multisigOutput.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.
   wallet.completeTx(req);
   Coin multisigFee = req.tx.getFee();
   multisigContract = 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(multisigOutput)
       .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;
   }
   refundTx.getConfidence().setSource(TransactionConfidence.Source.SELF);
   log.info(
       "initiated channel with multi-sig contract {}, refund {}",
       multisigContract.getHashAsString(),
       refundTx.getHashAsString());
   state = State.INITIATED;
   // Client should now call getIncompleteRefundTransaction() and send it to the server.
 }
 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);
   wallet.addOrUpdateExtension(channels);
   storedChannel = null;
 }
 private synchronized Transaction makeUnsignedChannelContract(Coin valueToMe)
     throws ValueOutOfRangeException {
   Transaction tx = new Transaction(wallet.getParams());
   tx.addInput(multisigContract.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;
 }
 @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, multisigContract.getHash()) == null);
   storedChannel =
       new StoredClientChannel(id, multisigContract, refundTx, myKey, valueToMe, refundFees, true);
   channels.putChannel(storedChannel);
   wallet.addOrUpdateExtension(channels);
 }
 private synchronized void initWalletListeners() {
   // Register a listener that watches out for the server closing the channel.
   if (storedChannel != null && storedChannel.close != null) {
     watchCloseConfirmations();
   }
   wallet.addEventListener(
       new AbstractWalletEventListener() {
         @Override
         public void onCoinsReceived(
             Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
           synchronized (PaymentChannelClientState.this) {
             if (multisigContract == null) return;
             if (isSettlementTransaction(tx)) {
               log.info(
                   "Close: transaction {} closed contract {}",
                   tx.getHash(),
                   multisigContract.getHash());
               // Record the fact that it was closed along with the transaction that closed it.
               state = State.CLOSED;
               if (storedChannel == null) return;
               storedChannel.close = tx;
               updateChannelInWallet();
               watchCloseConfirmations();
             }
           }
         }
       },
       Threading.SAME_THREAD);
 }
 private synchronized void updateChannelInWallet() {
   if (storedServerChannel != null) {
     storedServerChannel.updateValueToMe(bestValueToMe, bestValueSignature);
     StoredPaymentChannelServerStates channels =
         (StoredPaymentChannelServerStates)
             wallet.getExtensions().get(StoredPaymentChannelServerStates.EXTENSION_ID);
     channels.updatedChannel(storedServerChannel);
   }
 }
 // Create a payment transaction with valueToMe going back to us
 private synchronized Wallet.SendRequest makeUnsignedChannelContract(Coin valueToMe) {
   Transaction tx = new Transaction(wallet.getParams());
   if (!totalValue.subtract(valueToMe).equals(Coin.ZERO)) {
     clientOutput.setValue(totalValue.subtract(valueToMe));
     tx.addOutput(clientOutput);
   }
   tx.addInput(multisigContract.getOutput(0));
   return Wallet.SendRequest.forTx(tx);
 }
Ejemplo n.º 10
0
 /** Skips saving state in the wallet for testing */
 @VisibleForTesting
 synchronized void fakeSave() {
   try {
     wallet.commitTx(multisigContract);
   } catch (VerificationException e) {
     throw new RuntimeException(e); // We created it
   }
   state = State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER;
 }
Ejemplo n.º 11
0
 /** Returns true if this output is to a key, or an address we have the keys for, in the wallet. */
 public boolean isMine(Wallet wallet) {
   try {
     Script script = getScriptPubKey();
     if (script.isSentToRawPubKey()) {
       byte[] pubkey = script.getPubKey();
       return wallet.isPubKeyMine(pubkey);
     }
     if (script.isPayToScriptHash()) {
       return wallet.isPayToScriptHashMine(script.getPubKeyHash());
     } else {
       byte[] pubkeyHash = script.getPubKeyHash();
       return wallet.isPubKeyHashMine(pubkeyHash);
     }
   } catch (ScriptException e) {
     // Just means we didn't understand the output of this transaction: ignore it.
     log.debug("Could not parse tx output script: {}", e.toString());
     return false;
   }
 }
Ejemplo n.º 12
0
 /** Returns true if this output is to a key, or an address we have the keys for, in the wallet. */
 public boolean isWatched(Wallet wallet) {
   try {
     Script script = getScriptPubKey();
     return wallet.isWatchedScript(script);
   } catch (ScriptException e) {
     // Just means we didn't understand the output of this transaction: ignore it.
     log.debug("Could not parse tx output script: {}", e.toString());
     return false;
   }
 }
Ejemplo n.º 13
0
  /**
   * 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.
   *
   * <p>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) {
    checkState(state == State.SAVE_STATE_IN_WALLET && id != null);
    if (storedChannel != null) {
      checkState(storedChannel.id.equals(id));
      return;
    }
    doStoreChannelInWallet(id);

    try {
      wallet.commitTx(multisigContract);
    } catch (VerificationException e) {
      throw new RuntimeException(e); // We created it
    }
    state = State.PROVIDE_MULTISIG_CONTRACT_TO_SERVER;
  }
  /**
   * Stores this channel's state in the wallet as a part of a {@link
   * StoredPaymentChannelServerStates} wallet extension and keeps it up-to-date each time payment is
   * incremented. This will be automatically removed when a call to {@link
   * PaymentChannelServerState#close()} completes successfully. A channel may only be stored after
   * it has fully opened (ie state == State.READY).
   *
   * @param connectedHandler Optional {@link PaymentChannelServer} object that manages this object.
   *     This will set the appropriate pointer in the newly created {@link StoredServerChannel}
   *     before it is committed to wallet. If set, closing the state object will propagate the close
   *     to the handler which can then do a TCP disconnect.
   */
  public synchronized void storeChannelInWallet(@Nullable PaymentChannelServer connectedHandler) {
    checkState(state == State.READY);
    if (storedServerChannel != null) return;

    log.info("Storing state with contract hash {}.", multisigContract.getHash());
    StoredPaymentChannelServerStates channels =
        (StoredPaymentChannelServerStates)
            wallet.addOrGetExistingExtension(
                new StoredPaymentChannelServerStates(wallet, broadcaster));
    storedServerChannel =
        new StoredServerChannel(
            this,
            multisigContract,
            clientOutput,
            refundTransactionUnlockTimeSecs,
            serverKey,
            bestValueToMe,
            bestValueSignature);
    if (connectedHandler != null)
      checkState(
          storedServerChannel.setConnectedHandler(connectedHandler, false) == connectedHandler);
    channels.putChannel(storedServerChannel);
  }
  /**
   * Closes this channel and broadcasts the highest value payment transaction on the network.
   *
   * <p>This will set the state to {@link State#CLOSED} if the transaction is successfully broadcast
   * on the network. If we fail to broadcast for some reason, the state is set to {@link
   * State#ERROR}.
   *
   * <p>If the current state is before {@link State#READY} (ie we have not finished initializing the
   * channel), we simply set the state to {@link State#CLOSED} and let the client handle getting its
   * refund transaction confirmed.
   *
   * @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 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 (state.compareTo(State.CLOSING) >= 0) return closedFuture;
    }

    if (state.ordinal() < State.READY.ordinal()) {
      log.error("Attempt to settle channel in state " + state);
      state = State.CLOSED;
      closedFuture.set(null);
      return closedFuture;
    }
    if (state != State.READY) {
      // TODO: What is this codepath for?
      log.warn("Failed attempt to settle a channel in state " + state);
      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.
      signMultisigInput(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.
      signMultisigInput(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 {}",
          multisigContract,
          tx != null ? tx : "");
      throw new RuntimeException(e); // Should never happen.
    }
    state = 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());
            state = State.CLOSED;
            closedFuture.set(transaction);
          }

          @Override
          public void onFailure(Throwable throwable) {
            log.error("Failed to settle channel, could not broadcast", throwable);
            state = State.ERROR;
            closedFuture.setException(throwable);
          }
        });
    return closedFuture;
  }
  /**
   * Called when the client provides us with a new signature and wishes to increment total payment
   * by size. Verifies the provided signature and only updates values if everything checks out. If
   * the new refundSize is not the lowest we have seen, it is simply ignored.
   *
   * @param refundSize How many satoshis of the original contract are refunded to the client (the
   *     rest are ours)
   * @param signatureBytes The new signature spending the multi-sig contract to a new payment
   *     transaction
   * @throws VerificationException If the signature does not verify or size is out of range (incl
   *     being rejected by the network as dust).
   * @return true if there is more value left on the channel, false if it is now fully used up.
   */
  public synchronized boolean incrementPayment(Coin refundSize, byte[] signatureBytes)
      throws VerificationException, ValueOutOfRangeException, InsufficientMoneyException {
    checkState(state == State.READY);
    checkNotNull(refundSize);
    checkNotNull(signatureBytes);
    TransactionSignature signature = TransactionSignature.decodeFromBitcoin(signatureBytes, true);
    // We allow snapping to zero for the payment amount because it's treated specially later, but
    // not less than
    // the dust level because that would prevent the transaction from being relayed/mined.
    final boolean fullyUsedUp = refundSize.equals(Coin.ZERO);
    if (refundSize.compareTo(clientOutput.getMinNonDustValue()) < 0 && !fullyUsedUp)
      throw new ValueOutOfRangeException(
          "Attempt to refund negative value or value too small to be accepted by the network");
    Coin newValueToMe = totalValue.subtract(refundSize);
    if (newValueToMe.signum() < 0)
      throw new ValueOutOfRangeException("Attempt to refund more than the contract allows.");
    if (newValueToMe.compareTo(bestValueToMe) < 0)
      throw new ValueOutOfRangeException("Attempt to roll back payment on the channel.");

    // Get the wallet's copy of the multisigContract (ie with confidence information), if this is
    // null, the wallet
    // was not connected to the peergroup when the contract was broadcast (which may cause issues
    // down the road, and
    // disables our double-spend check next)
    Transaction walletContract = wallet.getTransaction(multisigContract.getHash());
    checkNotNull(
        walletContract,
        "Wallet did not contain multisig contract {} after state was marked READY",
        multisigContract.getHash());

    // Note that we check for DEAD state here, but this test is essentially useless in production
    // because we will
    // miss most double-spends due to bloom filtering right now anyway. This will eventually fixed
    // by network-wide
    // double-spend notifications, so we just wait instead of attempting to add all dependant
    // outpoints to our bloom
    // filters (and probably missing lots of edge-cases).
    if (walletContract.getConfidence().getConfidenceType()
        == TransactionConfidence.ConfidenceType.DEAD) {
      close();
      throw new VerificationException("Multisig contract was double-spent");
    }

    Transaction.SigHash mode;
    // If the client doesn't want anything back, they shouldn't sign any outputs at all.
    if (fullyUsedUp) mode = Transaction.SigHash.NONE;
    else mode = Transaction.SigHash.SINGLE;

    if (signature.sigHashMode() != mode || !signature.anyoneCanPay())
      throw new VerificationException(
          "New payment signature was not signed with the right SIGHASH flags.");

    Wallet.SendRequest req = makeUnsignedChannelContract(newValueToMe);
    // Now check the signature is correct.
    // Note that the client must sign with SIGHASH_{SINGLE/NONE} | SIGHASH_ANYONECANPAY to allow us
    // to add additional
    // inputs (in case we need to add significant fee, or something...) and any outputs we want to
    // pay to.
    Sha256Hash sighash = req.tx.hashForSignature(0, multisigScript, mode, true);

    if (!clientKey.verify(sighash, signature))
      throw new VerificationException("Signature does not verify on tx\n" + req.tx);
    bestValueToMe = newValueToMe;
    bestValueSignature = signatureBytes;
    updateChannelInWallet();
    return !fullyUsedUp;
  }