Source:
<code>private void processTxFromBestChain(Transaction tx, boolean forceAddToPool) throws VerificationException { checkState(lock.isHeldByCurrentThread()); checkState(!pending.containsKey(tx.getHash())); boolean isDeadCoinbase = tx.isCoinBase() && dead.containsKey(tx.getHash()); if (isDeadCoinbase) { log.info(" coinbase tx <-dead: confidence {}", tx.getHashAsString(), tx.getConfidence().getConfidenceType().name()); dead.remove(tx.getHash()); } updateForSpends(tx, true); boolean hasOutputsToMe = tx.getValueSentToMe(this).signum() > 0; if (hasOutputsToMe) { if (tx.isEveryOwnedOutputSpent(this)) { log.info(" tx ->spent (by pending)", tx.getHashAsString()); addWalletTransaction(Pool.SPENT, tx); } else { log.info(" tx ->unspent", tx.getHashAsString()); addWalletTransaction(Pool.UNSPENT, tx); } } else if (tx.getValueSentFromMe(this).signum() > 0) { log.info(" tx ->spent", tx.getHashAsString()); addWalletTransaction(Pool.SPENT, tx); } else if (forceAddToPool) { log.info(" tx ->spent (manually added)", tx.getHashAsString()); addWalletTransaction(Pool.SPENT, tx); } Set<Transaction> doubleSpendTxns = findDoubleSpendsAgainst(tx, pending); if (!doubleSpendTxns.isEmpty()) { killTxns(doubleSpendTxns, tx); <START> for(TransactionInput input : tx.getInputs()) { <END> TransactionOutput output = input.getOutpoint().getConnectedOutput(); if(output != null && !output.isMine(this)) { input.disconnect(); } } } }</code><technical_language>more thought this fix: if double spending transaction *is* relevant wallet (either double-spent UTXO indirectly somehow). Is ok disconnect inputs? Do a test for that</technical_language>

Target:
private void processTxFromBestChain(Transaction tx, boolean forceAddToPool) throws VerificationException { checkState(lock.isHeldByCurrentThread()); checkState(!pending.containsKey(tx.getHash())); boolean isDeadCoinbase = tx.isCoinBase() && dead.containsKey(tx.getHash()); if (isDeadCoinbase) { log.info(" coinbase tx <-dead: confidence {}", tx.getHashAsString(), tx.getConfidence().getConfidenceType().name()); dead.remove(tx.getHash()); } updateForSpends(tx, true); boolean hasOutputsToMe = tx.getValueSentToMe(this).signum() > 0; boolean hasOutputsFromMe = false; if (hasOutputsToMe) { if (tx.isEveryOwnedOutputSpent(this)) { log.info(" tx ->spent (by pending)", tx.getHashAsString()); addWalletTransaction(Pool.SPENT, tx); } else { log.info(" tx ->unspent", tx.getHashAsString()); addWalletTransaction(Pool.UNSPENT, tx); } } else if (tx.getValueSentFromMe(this).signum() > 0) { hasOutputsFromMe = true; log.info(" tx ->spent", tx.getHashAsString()); addWalletTransaction(Pool.SPENT, tx); } else if (forceAddToPool) { log.info(" tx ->spent (manually added)", tx.getHashAsString()); addWalletTransaction(Pool.SPENT, tx); } Set<Transaction> doubleSpendTxns = findDoubleSpendsAgainst(tx, pending); if (!doubleSpendTxns.isEmpty()) { killTxns(doubleSpendTxns, tx); } if( !hasOutputsToMe && !hasOutputsFromMe && !forceAddToPool && !findDoubleSpendsAgainst(tx, transactions).isEmpty()) { for (TransactionInput input : tx.getInputs()) { TransactionOutput output = input.getConnectedOutput(); if(output != null && !output.isMineOrWatched(this)) { input.disconnect(); } } } }