Fix for irrelevant double spend causing protobuf serialization issues

Fixes #1374. Previously, transactions could have spentBy set but the spending transaction
dropped as being irrelevant. This would then be serialized to protobuf but could not be
deserialized.
This commit is contained in:
Saulius Beinorius
2017-05-19 14:28:53 +03:00
committed by Andreas Schildbach
parent 9bde092143
commit 943a139d08
2 changed files with 97 additions and 0 deletions

View File

@@ -1820,6 +1820,8 @@ public class Wallet extends BaseTaggableObject
// Now for each pending transaction, see if it shares any outpoints with this tx.
Set<Transaction> doubleSpendTxns = Sets.newHashSet();
for (Transaction p : candidates.values()) {
if (p.equals(tx))
continue;
for (TransactionInput input : p.getInputs()) {
// This relies on the fact that TransactionOutPoint equality is defined at the protocol not object
// level - outpoints from two different inputs that point to the same output compare the same.
@@ -2185,6 +2187,7 @@ public class Wallet extends BaseTaggableObject
// against our pending transactions. Note that a tx may double spend our pending transactions and also send
// us money/spend our money.
boolean hasOutputsToMe = tx.getValueSentToMe(this).signum() > 0;
boolean hasOutputsFromMe = false;
if (hasOutputsToMe) {
// Needs to go into either unspent or spent (if the outputs were already spent by a pending tx).
if (tx.isEveryOwnedOutputSpent(this)) {
@@ -2195,6 +2198,7 @@ public class Wallet extends BaseTaggableObject
addWalletTransaction(Pool.UNSPENT, tx);
}
} else if (tx.getValueSentFromMe(this).signum() > 0) {
hasOutputsFromMe = true;
// Didn't send us any money, but did spend some. Keep it around for record keeping purposes.
log.info(" tx {} ->spent", tx.getHashAsString());
addWalletTransaction(Pool.SPENT, tx);
@@ -2210,6 +2214,19 @@ public class Wallet extends BaseTaggableObject
// no need to addTransactionsDependingOn(doubleSpendTxns) because killTxns() already kills dependencies;
killTxns(doubleSpendTxns, tx);
}
if (!hasOutputsToMe
&& !hasOutputsFromMe
&& !forceAddToPool
&& !findDoubleSpendsAgainst(tx, transactions).isEmpty())
{
// disconnect irrelevant inputs (otherwise might cause protobuf serialization issue)
for (TransactionInput input : tx.getInputs()) {
TransactionOutput output = input.getConnectedOutput();
if (output != null && !output.isMineOrWatched(this)) {
input.disconnect();
}
}
}
}
/**

View File

@@ -3441,4 +3441,84 @@ public class WalletTest extends TestWithWallet {
// TODO: test shared wallet calculation here
}
@Test
public void testIrrelevantDoubleSpend() throws Exception {
Transaction tx0 = createFakeTx(PARAMS);
Transaction tx1 = createFakeTx(PARAMS);
Transaction tx2 = new Transaction(PARAMS);
tx2.addInput(tx0.getOutput(0));
tx2.addOutput(COIN, myAddress);
tx2.addOutput(COIN, OTHER_ADDRESS);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, tx2, tx1, tx0);
// tx3 and tx4 double spend each other
Transaction tx3 = new Transaction(PARAMS);
tx3.addInput(tx1.getOutput(0));
tx3.addOutput(COIN, myAddress);
tx3.addOutput(COIN, OTHER_ADDRESS);
wallet.receivePending(tx3, null);
// tx4 also spends irrelevant output from tx2
Transaction tx4 = new Transaction(PARAMS);
tx4.addInput(tx1.getOutput(0)); // spends same output
tx4.addInput(tx2.getOutput(1));
tx4.addOutput(COIN, OTHER_ADDRESS);
// tx4 does not actually get added to wallet here since it by itself is irrelevant
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, tx4);
// since tx4 is not saved, tx2 output 1 will have bad spentBy
wallet = roundTrip(wallet);
assertTrue(wallet.isConsistent());
}
@Test
public void overridingDeadTxTest() throws Exception {
Transaction tx0 = createFakeTx(PARAMS);
Transaction tx1 = new Transaction(PARAMS);
tx1.addInput(tx0.getOutput(0));
tx1.addOutput(COIN, OTHER_ADDRESS);
tx1.addOutput(COIN, OTHER_ADDRESS);
tx1.addOutput(COIN, myAddress); // to save this in wallet
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, tx0, tx1);
// tx2, tx3 and tx4 double spend each other
Transaction tx2 = new Transaction(PARAMS);
tx2.addInput(tx1.getOutput(0));
tx2.addInput(tx1.getOutput(1));
tx2.addOutput(COIN, myAddress);
tx2.addOutput(COIN, OTHER_ADDRESS);
wallet.receivePending(tx2, null);
// irrelevant to the wallet
Transaction tx3 = new Transaction(PARAMS);
tx3.addInput(tx1.getOutput(0)); // spends same output as tx2
tx3.addOutput(COIN, OTHER_ADDRESS);
// irrelevant to the wallet
Transaction tx4 = new Transaction(PARAMS);
tx4.addInput(tx1.getOutput(1)); // spends different output, but also in tx2
tx4.addOutput(COIN, OTHER_ADDRESS);
assertUnspent(tx1);
assertPending(tx2);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, tx3);
assertUnspent(tx1);
assertDead(tx2);
assertEquals(2, wallet.transactions.size()); // tx3 not saved
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, tx4);
assertUnspent(tx1);
assertDead(tx2);
assertEquals(2, wallet.transactions.size()); // tx4 not saved
// this will fail if tx4 does not get disconnected from tx1
wallet = roundTrip(wallet);
assertTrue(wallet.isConsistent());
}
}