/** * 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); }
/** 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; }
/** 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; } }
/** 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; } }
/** * 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; }