Fix RBF after chained replaceable txs
MODIFY
WalletWasabi.Tests/UnitTests/Transactions/TransactionProcessorTests.cs
WalletWasabi.Tests/UnitTests/Transactions/TransactionProcessorTests.cs
@@ -191,9 +191,14 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
[Fact]
public async Task HandlesRbfAsync()
{
+ // --tx0---> (A) --tx1 (replaceable)-+--> (B) --tx2---> (D)
+ // | |
+ // | +--> (C)
+ // |
+ // +--tx4 (replacement)---> (E)
+ //
var transactionProcessor = await CreateTransactionProcessorAsync();
- var keys = transactionProcessor.KeyManager.GetKeys().ToArray();
int replaceTransactionReceivedCalled = 0;
transactionProcessor.ReplaceTransactionReceived += (s, e) =>
{
@@ -202,34 +207,50 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
Assert.Equal(Money.Coins(1.0m), originalCoin.Amount);
// Remove the created coin by the transaction.
- var createdCoin = Assert.Single(e.DestroyedCoins);
- Assert.Equal(Money.Coins(0.95m), createdCoin.Amount);
+ Assert.Equal(3, e.DestroyedCoins.Count());
+ Assert.Single(e.DestroyedCoins, coin => coin.HdPubKey.Label == "B");
+ Assert.Single(e.DestroyedCoins, coin => coin.HdPubKey.Label == "C");
+ Assert.Single(e.DestroyedCoins, coin => coin.HdPubKey.Label == "D");
replaceTransactionReceivedCalled++;
};
// A confirmed segwit transaction for us
- var tx = CreateCreditingTransaction(keys[0].PubKey.WitHash.ScriptPubKey, Money.Coins(1.0m), height: 54321);
+ var tx = CreateCreditingTransaction(transactionProcessor.NewKey("A").P2wpkhScript, Money.Coins(1.0m), height: 54321);
transactionProcessor.Process(tx);
var createdCoin = tx.Transaction.Outputs.AsCoins().First();
// Spend the received coin
- tx = CreateSpendingTransaction(createdCoin, keys[1].PubKey.WitHash.ScriptPubKey);
+ tx = CreateSpendingTransaction(createdCoin, transactionProcessor.NewKey("B").P2wpkhScript);
tx.Transaction.Inputs[0].Sequence = Sequence.OptInRBF;
tx.Transaction.Outputs[0].Value = Money.Coins(0.95m);
+ tx.Transaction.Outputs.Add(Money.Coins(0.1m), transactionProcessor.NewKey("C").P2wpkhScript);
var relevant = transactionProcessor.Process(tx);
Assert.True(relevant);
Assert.Equal(0, replaceTransactionReceivedCalled);
+ var unconfirmedCoin1 = Assert.Single(transactionProcessor.Coins, coin => coin.HdPubKey.Label == "B");
+ var unconfirmedCoin2 = Assert.Single(transactionProcessor.Coins, coin => coin.HdPubKey.Label == "C");
+ Assert.True(unconfirmedCoin1.IsReplaceable);
+ Assert.True(unconfirmedCoin2.IsReplaceable);
+
+ // Spend the received coin
+ tx = CreateSpendingTransaction(unconfirmedCoin1.GetCoin(), transactionProcessor.NewKey("D").P2wpkhScript);
+ tx.Transaction.Outputs[0].Value = Money.Coins(0.7m);
+ relevant = transactionProcessor.Process(tx);
+ Assert.True(relevant);
+ Assert.Equal(0, replaceTransactionReceivedCalled);
+
// Spend it coin
- tx = CreateSpendingTransaction(createdCoin, keys[2].PubKey.WitHash.ScriptPubKey);
+ tx = CreateSpendingTransaction(createdCoin, transactionProcessor.NewKey("E").P2wpkhScript);
tx.Transaction.Outputs[0].Value = Money.Coins(0.9m);
relevant = transactionProcessor.Process(tx);
Assert.Equal(1, replaceTransactionReceivedCalled);
Assert.True(relevant);
- Assert.Single(transactionProcessor.Coins, coin => coin.Amount == Money.Coins(0.9m) && coin.IsReplaceable);
- Assert.Single(transactionProcessor.Coins.AsAllCoinsView(), coin => !coin.Unspent && coin.Amount == Money.Coins(1.0m) && !coin.IsReplaceable);
+ var finalCoin = Assert.Single(transactionProcessor.Coins);
+ Assert.True(finalCoin.IsReplaceable);
+ Assert.Equal("E", finalCoin.HdPubKey.Label);
}
[Fact]

MODIFY
WalletWasabi/Coins/CoinsRegistry.cs
WalletWasabi/Coins/CoinsRegistry.cs
@@ -97,11 +97,11 @@ namespace WalletWasabi.Coins
return added;
}
- public void Remove(SmartCoin coin)
+ public ICoinsView Remove(SmartCoin coin)
{
lock (Lock)
{
- var coinsToRemove = DescendantOfAndSelfNoLock(coin).ToList();
+ var coinsToRemove = DescendantOfAndSelfNoLock(coin);
foreach (var toRemove in coinsToRemove)
{
if (Coins.Remove(toRemove))
@@ -110,6 +110,7 @@ namespace WalletWasabi.Coins
}
}
InvalidateSnapshot = true;
+ return coinsToRemove;
}
}
@@ -169,23 +170,25 @@ namespace WalletWasabi.Coins
{
lock (Lock)
{
- var toRemove = CreatedByNoLock(txId);
- var toAdd = SpentByNoLock(txId);
+ var allCoins = AsAllCoinsViewNoLock();
+ var toRemove = new List<SmartCoin>();
+ var toAdd = new List<SmartCoin>();
// remove recursively the coins created by the transaction
- foreach (SmartCoin createdCoin in toRemove)
+ foreach (SmartCoin createdCoin in allCoins.CreatedBy(txId))
{
- Coins.Remove(createdCoin);
+ toRemove.AddRange(Remove(createdCoin));
}
// destroyed (spent) coins are now (unspent)
- foreach (SmartCoin destroyedCoin in toAdd)
+ foreach (SmartCoin destroyedCoin in allCoins.SpentBy(txId))
{
if (SpentCoins.Remove(destroyedCoin))
{
Coins.Add(destroyedCoin);
+ toAdd.Add(destroyedCoin);
}
}
- return (toRemove, toAdd);
+ return (new CoinsView(toRemove), new CoinsView(toAdd));
}
}

MODIFY
WalletWasabi/Transactions/TransactionProcessor.cs
WalletWasabi/Transactions/TransactionProcessor.cs
@@ -97,7 +97,7 @@ namespace WalletWasabi.Transactions
if (!tx.Transaction.IsCoinBase) // Transactions we already have and processed would be "double spends" but they shouldn't.
{
var doubleSpends = new List<SmartCoin>();
- foreach (SmartCoin coin in Coins)
+ foreach (SmartCoin coin in Coins.AsAllCoinsView())
{
var spent = false;
foreach (TxoRef spentOutput in coin.SpentOutputs)

Fix RBF after chained replaceable txs